mach_commands.py (9817B)
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 import logging 6 import os 7 import pathlib 8 import time 9 10 import mozinfo 11 from mach.decorators import Command, CommandArgument 12 from mozbuild.base import BinaryNotFoundException 13 from mozbuild.base import MachCommandConditions as conditions 14 from mozfile import json 15 16 17 def is_valgrind_build(cls): 18 """Must be a build with --enable-valgrind and --disable-jemalloc.""" 19 defines = cls.config_environment.defines 20 return "MOZ_VALGRIND" in defines and "MOZ_MEMORY" not in defines 21 22 23 @Command( 24 "valgrind-test", 25 category="testing", 26 conditions=[conditions.is_firefox_or_thunderbird, is_valgrind_build], 27 description="Run the Valgrind test job (memory-related errors).", 28 ) 29 @CommandArgument( 30 "--suppressions", 31 default=[], 32 action="append", 33 metavar="FILENAME", 34 help="Specify a suppression file for Valgrind to use. Use " 35 "--suppression multiple times to specify multiple suppression " 36 "files.", 37 ) 38 def valgrind_test(command_context, suppressions): 39 """ 40 Run Valgrind tests. 41 """ 42 43 from mozfile import TemporaryDirectory 44 from mozhttpd import MozHttpd 45 from mozprofile import FirefoxProfile, Preferences 46 from mozprofile.permissions import ServerLocations 47 from mozrunner import FirefoxRunner 48 from mozrunner.utils import findInPath 49 from valgrind.output_handler import OutputHandler 50 51 build_dir = os.path.join(command_context.topsrcdir, "build") 52 53 # XXX: currently we just use the PGO inputs for Valgrind runs. This may 54 # change in the future. 55 httpd = MozHttpd(docroot=os.path.join(build_dir, "pgo")) 56 httpd.start(block=False) 57 58 with TemporaryDirectory() as profilePath: 59 # TODO: refactor this into mozprofile 60 profile_data_dir = os.path.join( 61 command_context.topsrcdir, "testing", "profiles" 62 ) 63 with open(os.path.join(profile_data_dir, "profiles.json")) as fh: 64 base_profiles = json.load(fh)["valgrind"] 65 66 prefpaths = [ 67 os.path.join(profile_data_dir, profile, "user.js") 68 for profile in base_profiles 69 ] 70 prefs = {} 71 for path in prefpaths: 72 prefs.update(Preferences.read_prefs(path)) 73 74 interpolation = { 75 "server": "%s:%d" % httpd.httpd.server_address, 76 } 77 for k, v in prefs.items(): 78 if isinstance(v, str): 79 v = v.format(**interpolation) 80 prefs[k] = Preferences.cast(v) 81 82 quitter = os.path.join( 83 command_context.topsrcdir, "tools", "quitter", "quitter@mozilla.org.xpi" 84 ) 85 86 locations = ServerLocations() 87 locations.add_host( 88 host="127.0.0.1", port=httpd.httpd.server_port, options="primary" 89 ) 90 91 profile = FirefoxProfile( 92 profile=profilePath, 93 preferences=prefs, 94 addons=[quitter], 95 locations=locations, 96 ) 97 98 firefox_args = [httpd.get_url()] 99 100 env = os.environ.copy() 101 env["G_SLICE"] = "always-malloc" 102 env["MOZ_FORCE_DISABLE_E10S"] = "1" 103 env["MOZ_CC_RUN_DURING_SHUTDOWN"] = "1" 104 env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" 105 env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" 106 env["XPCOM_DEBUG_BREAK"] = "warn" 107 108 outputHandler = OutputHandler(command_context.log) 109 kp_kwargs = { 110 "processOutputLine": [outputHandler], 111 "universal_newlines": True, 112 } 113 114 valgrind = "valgrind" 115 if not os.path.exists(valgrind): 116 valgrind = findInPath(valgrind) 117 118 valgrind_args = [ 119 valgrind, 120 "--sym-offsets=yes", 121 "--smc-check=all-non-file", 122 "--vex-iropt-register-updates=allregs-at-mem-access", 123 "--gen-suppressions=all", 124 "--num-callers=36", 125 "--leak-check=full", 126 "--show-possibly-lost=no", 127 "--track-origins=yes", 128 "--trace-children=yes", 129 "--trace-children-skip=*/dbus-launch", 130 "-v", # Enable verbosity to get the list of used suppressions 131 # Avoid excessive delays in the presence of spinlocks. 132 # See bug 1309851. 133 "--fair-sched=yes", 134 # Keep debuginfo after library unmap. See bug 1382280. 135 "--keep-debuginfo=yes", 136 # Reduce noise level on rustc and/or LLVM compiled code. 137 # See bug 1365915 138 "--expensive-definedness-checks=yes", 139 # Compensate for the compiler inlining `new` but not `delete` 140 # or vice versa. 141 "--show-mismatched-frees=no", 142 ] 143 144 for s in suppressions: 145 valgrind_args.append("--suppressions=" + s) 146 147 supps_dir = os.path.join(build_dir, "valgrind") 148 supps_file1 = os.path.join(supps_dir, "cross-architecture.sup") 149 valgrind_args.append("--suppressions=" + supps_file1) 150 151 if mozinfo.os == "linux": 152 machtype = { 153 "x86_64": "x86_64-pc-linux-gnu", 154 "x86": "i386-pc-linux-gnu", 155 }.get(mozinfo.processor) 156 if machtype: 157 supps_file2 = os.path.join(supps_dir, machtype + ".sup") 158 if os.path.isfile(supps_file2): 159 valgrind_args.append("--suppressions=" + supps_file2) 160 161 exitcode = None 162 timeout = 3600 163 binary_not_found_exception = None 164 try: 165 runner = FirefoxRunner( 166 profile=profile, 167 binary=command_context.get_binary_path(), 168 cmdargs=firefox_args, 169 env=env, 170 process_args=kp_kwargs, 171 ) 172 start_time = time.monotonic() 173 runner.start(debug_args=valgrind_args) 174 exitcode = runner.wait(timeout=timeout) 175 end_time = time.monotonic() 176 if "MOZ_AUTOMATION" in os.environ: 177 data = { 178 "framework": {"name": "build_metrics"}, 179 "suites": [ 180 { 181 "name": "valgrind", 182 "value": end_time - start_time, 183 "lowerIsBetter": True, 184 "shouldAlert": False, 185 "subtests": [], 186 } 187 ], 188 } 189 if "TASKCLUSTER_INSTANCE_TYPE" in os.environ: 190 # Include the instance type so results can be grouped. 191 data["suites"][0]["extraOptions"] = [ 192 "taskcluster-%s" % os.environ["TASKCLUSTER_INSTANCE_TYPE"], 193 ] 194 command_context.log( 195 logging.INFO, 196 "valgrind-perfherder", 197 {"data": json.dumps(data)}, 198 "PERFHERDER_DATA: {data}", 199 ) 200 upload_path = pathlib.Path(os.environ.get("MOZ_PERFHERDER_UPLOAD")) 201 upload_path.parent.mkdir(parents=True, exist_ok=True) 202 with upload_path.open("w", encoding="utf-8") as f: 203 json.dump(data, f) 204 205 except BinaryNotFoundException as e: 206 binary_not_found_exception = e 207 finally: 208 errs = outputHandler.error_count 209 supps = outputHandler.suppression_count 210 if errs != supps: 211 status = 1 # turns the TBPL job orange 212 command_context.log( 213 logging.ERROR, 214 "valgrind-fail-parsing", 215 {"errs": errs, "supps": supps}, 216 "TEST-UNEXPECTED-FAIL | valgrind-test | error parsing: {errs} errors " 217 "seen, but {supps} generated suppressions seen", 218 ) 219 220 elif errs == 0: 221 status = 0 222 command_context.log( 223 logging.INFO, 224 "valgrind-pass", 225 {}, 226 "TEST-PASS | valgrind-test | valgrind found no errors", 227 ) 228 else: 229 status = 1 # turns the TBPL job orange 230 # We've already printed details of the errors. 231 232 if binary_not_found_exception: 233 status = 2 # turns the TBPL job red 234 command_context.log( 235 logging.ERROR, 236 "valgrind-fail-errors", 237 {"error": str(binary_not_found_exception)}, 238 "TEST-UNEXPECTED-FAIL | valgrind-test | {error}", 239 ) 240 command_context.log( 241 logging.INFO, 242 "valgrind-fail-errors", 243 {"help": binary_not_found_exception.help()}, 244 "{help}", 245 ) 246 elif exitcode is None: 247 status = 2 # turns the TBPL job red 248 command_context.log( 249 logging.ERROR, 250 "valgrind-fail-timeout", 251 {"timeout": timeout}, 252 "TEST-UNEXPECTED-FAIL | valgrind-test | Valgrind timed out " 253 "(reached {timeout} second limit)", 254 ) 255 elif exitcode != 0: 256 status = 2 # turns the TBPL job red 257 command_context.log( 258 logging.ERROR, 259 "valgrind-fail-errors", 260 {"exitcode": exitcode}, 261 "TEST-UNEXPECTED-FAIL | valgrind-test | non-zero exit code " 262 "from Valgrind: {exitcode}", 263 ) 264 265 httpd.stop() 266 267 return status