1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# 4# Copyright 2016 The ChromiumOS Authors 5# Use of this source code is governed by a BSD-style license that can be 6# found in the LICENSE file. 7 8"""Script for running nightly compiler tests on ChromeOS. 9 10This script launches a buildbot to build ChromeOS with the latest compiler on 11a particular board; then it finds and downloads the trybot image and the 12corresponding official image, and runs crosperf performance tests comparing 13the two. It then generates a report, emails it to the c-compiler-chrome, as 14well as copying the images into the seven-day reports directory. 15""" 16 17# Script to test different toolchains against ChromeOS benchmarks. 18 19 20import argparse 21import datetime 22import os 23import re 24import shutil 25import sys 26import time 27 28from cros_utils import buildbot_utils 29from cros_utils import command_executer 30from cros_utils import logger 31 32 33CROSTC_ROOT = "/usr/local/google/crostc" 34NIGHTLY_TESTS_DIR = os.path.join(CROSTC_ROOT, "nightly-tests") 35ROLE_ACCOUNT = "mobiletc-prebuild" 36TOOLCHAIN_DIR = os.path.dirname(os.path.realpath(__file__)) 37TMP_TOOLCHAIN_TEST = "/tmp/toolchain-tests" 38MAIL_PROGRAM = "~/var/bin/mail-detective" 39PENDING_ARCHIVES_DIR = os.path.join(CROSTC_ROOT, "pending_archives") 40NIGHTLY_TESTS_RESULTS = os.path.join(CROSTC_ROOT, "nightly_test_reports") 41 42IMAGE_DIR = "{board}-{image_type}" 43IMAGE_VERSION_STR = r"{chrome_version}-{tip}\.{branch}\.{branch_branch}" 44IMAGE_FS = IMAGE_DIR + "/" + IMAGE_VERSION_STR 45TRYBOT_IMAGE_FS = IMAGE_FS + "-{build_id}" 46IMAGE_RE_GROUPS = { 47 "board": r"(?P<board>\S+)", 48 "image_type": r"(?P<image_type>\S+)", 49 "chrome_version": r"(?P<chrome_version>R\d+)", 50 "tip": r"(?P<tip>\d+)", 51 "branch": r"(?P<branch>\d+)", 52 "branch_branch": r"(?P<branch_branch>\d+)", 53 "build_id": r"(?P<build_id>b\d+)", 54} 55TRYBOT_IMAGE_RE = TRYBOT_IMAGE_FS.format(**IMAGE_RE_GROUPS) 56 57RECIPE_IMAGE_FS = IMAGE_FS + "-{build_id}-{buildbucket_id}" 58RECIPE_IMAGE_RE_GROUPS = { 59 "board": r"(?P<board>\S+)", 60 "image_type": r"(?P<image_type>\S+)", 61 "chrome_version": r"(?P<chrome_version>R\d+)", 62 "tip": r"(?P<tip>\d+)", 63 "branch": r"(?P<branch>\d+)", 64 "branch_branch": r"(?P<branch_branch>\d+)", 65 "build_id": r"(?P<build_id>\d+)", 66 "buildbucket_id": r"(?P<buildbucket_id>\d+)", 67} 68RECIPE_IMAGE_RE = RECIPE_IMAGE_FS.format(**RECIPE_IMAGE_RE_GROUPS) 69 70# CL that uses LLVM-Next to build the images (includes chrome). 71USE_LLVM_NEXT_PATCH = "513590" 72 73 74class ToolchainComparator(object): 75 """Class for doing the nightly tests work.""" 76 77 def __init__( 78 self, 79 board, 80 remotes, 81 chromeos_root, 82 weekday, 83 patches, 84 recipe=False, 85 test=False, 86 noschedv2=False, 87 ): 88 self._board = board 89 self._remotes = remotes 90 self._chromeos_root = chromeos_root 91 self._base_dir = os.getcwd() 92 self._ce = command_executer.GetCommandExecuter() 93 self._l = logger.GetLogger() 94 self._build = "%s-release-tryjob" % board 95 self._patches = patches.split(",") if patches else [] 96 self._patches_string = "_".join(str(p) for p in self._patches) 97 self._recipe = recipe 98 self._test = test 99 self._noschedv2 = noschedv2 100 101 if not weekday: 102 self._weekday = time.strftime("%a") 103 else: 104 self._weekday = weekday 105 self._date = datetime.date.today().strftime("%Y/%m/%d") 106 timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") 107 self._reports_dir = os.path.join( 108 TMP_TOOLCHAIN_TEST if self._test else NIGHTLY_TESTS_RESULTS, 109 "%s.%s" % (timestamp, board), 110 ) 111 112 def _GetVanillaImageName(self, trybot_image): 113 """Given a trybot artifact name, get latest vanilla image name. 114 115 Args: 116 trybot_image: artifact name such as 117 'daisy-release-tryjob/R40-6394.0.0-b1389' 118 for recipe images, name is in this format: 119 'lulu-llvm-next-nightly/R84-13037.0.0-31011-8883172717979984032/' 120 121 Returns: 122 Latest official image name, e.g. 'daisy-release/R57-9089.0.0'. 123 """ 124 # For board names with underscores, we need to fix the trybot image name 125 # to replace the hyphen (for the recipe builder) with the underscore. 126 # Currently the only such board we use is 'veyron_tiger'. 127 if trybot_image.find("veyron-tiger") != -1: 128 trybot_image = trybot_image.replace("veyron-tiger", "veyron_tiger") 129 # We need to filter out -tryjob in the trybot_image. 130 if self._recipe: 131 trybot = re.sub("-llvm-next-nightly", "-release", trybot_image) 132 mo = re.search(RECIPE_IMAGE_RE, trybot) 133 else: 134 trybot = re.sub("-tryjob", "", trybot_image) 135 mo = re.search(TRYBOT_IMAGE_RE, trybot) 136 assert mo 137 dirname = IMAGE_DIR.replace("\\", "").format(**mo.groupdict()) 138 return buildbot_utils.GetLatestImage(self._chromeos_root, dirname) 139 140 def _TestImages(self, trybot_image, vanilla_image): 141 """Create crosperf experiment file. 142 143 Given the names of the trybot, vanilla and non-AFDO images, create the 144 appropriate crosperf experiment file and launch crosperf on it. 145 """ 146 if self._test: 147 experiment_file_dir = TMP_TOOLCHAIN_TEST 148 else: 149 experiment_file_dir = os.path.join(NIGHTLY_TESTS_DIR, self._weekday) 150 experiment_file_name = "%s_toolchain_experiment.txt" % self._board 151 152 compiler_string = "llvm" 153 if USE_LLVM_NEXT_PATCH in self._patches_string: 154 experiment_file_name = "%s_llvm_next_experiment.txt" % self._board 155 compiler_string = "llvm_next" 156 157 experiment_file = os.path.join( 158 experiment_file_dir, experiment_file_name 159 ) 160 experiment_header = """ 161 board: %s 162 remote: %s 163 retries: 1 164 """ % ( 165 self._board, 166 self._remotes, 167 ) 168 # TODO(b/244607231): Add graphic benchmarks removed in crrev.com/c/3869851. 169 experiment_tests = """ 170 benchmark: all_toolchain_perf { 171 suite: telemetry_Crosperf 172 iterations: 5 173 run_local: False 174 } 175 176 benchmark: loading.desktop { 177 suite: telemetry_Crosperf 178 test_args: --story-tag-filter=typical 179 iterations: 3 180 run_local: False 181 retries: 0 182 } 183 """ 184 185 with open(experiment_file, "w", encoding="utf-8") as f: 186 f.write(experiment_header) 187 f.write(experiment_tests) 188 189 # Now add vanilla to test file. 190 official_image = """ 191 vanilla_image { 192 chromeos_root: %s 193 build: %s 194 compiler: llvm 195 } 196 """ % ( 197 self._chromeos_root, 198 vanilla_image, 199 ) 200 f.write(official_image) 201 202 label_string = "%s_trybot_image" % compiler_string 203 204 # Reuse autotest files from vanilla image for trybot images 205 autotest_files = os.path.join( 206 "/tmp", vanilla_image, "autotest_files" 207 ) 208 experiment_image = """ 209 %s { 210 chromeos_root: %s 211 build: %s 212 autotest_path: %s 213 compiler: %s 214 } 215 """ % ( 216 label_string, 217 self._chromeos_root, 218 trybot_image, 219 autotest_files, 220 compiler_string, 221 ) 222 f.write(experiment_image) 223 224 crosperf = os.path.join(TOOLCHAIN_DIR, "crosperf", "crosperf") 225 noschedv2_opts = "--noschedv2" if self._noschedv2 else "" 226 no_email = not self._test 227 command = ( 228 f"{crosperf} --no_email={no_email} " 229 f"--results_dir={self._reports_dir} --logging_level=verbose " 230 f"--json_report=True {noschedv2_opts} {experiment_file}" 231 ) 232 233 return self._ce.RunCommand(command) 234 235 def _SendEmail(self): 236 """Find email message generated by crosperf and send it.""" 237 filename = os.path.join(self._reports_dir, "msg_body.html") 238 if os.path.exists(filename) and os.path.exists( 239 os.path.expanduser(MAIL_PROGRAM) 240 ): 241 email_title = "buildbot llvm test results" 242 if USE_LLVM_NEXT_PATCH in self._patches_string: 243 email_title = "buildbot llvm_next test results" 244 command = 'cat %s | %s -s "%s, %s %s" -team -html' % ( 245 filename, 246 MAIL_PROGRAM, 247 email_title, 248 self._board, 249 self._date, 250 ) 251 self._ce.RunCommand(command) 252 253 def _CopyJson(self): 254 # Make sure a destination directory exists. 255 os.makedirs(PENDING_ARCHIVES_DIR, exist_ok=True) 256 # Copy json report to pending archives directory. 257 command = "cp %s/*.json %s/." % ( 258 self._reports_dir, 259 PENDING_ARCHIVES_DIR, 260 ) 261 ret = self._ce.RunCommand(command) 262 # Failing to access json report means that crosperf terminated or all tests 263 # failed, raise an error. 264 if ret != 0: 265 raise RuntimeError( 266 "Crosperf failed to run tests, cannot copy json report!" 267 ) 268 269 def DoAll(self): 270 """Main function inside ToolchainComparator class. 271 272 Launch trybot, get image names, create crosperf experiment file, run 273 crosperf, and copy images into seven-day report directories. 274 """ 275 if self._recipe: 276 print("Using recipe buckets to get latest image.") 277 # crbug.com/1077313: Some boards are not consistently 278 # spelled, having underscores in some places and dashes in others. 279 # The image directories consistenly use dashes, so convert underscores 280 # to dashes to work around this. 281 trybot_image = buildbot_utils.GetLatestRecipeImage( 282 self._chromeos_root, 283 "%s-llvm-next-nightly" % self._board.replace("_", "-"), 284 ) 285 else: 286 # Launch tryjob and wait to get image location. 287 buildbucket_id, trybot_image = buildbot_utils.GetTrybotImage( 288 self._chromeos_root, 289 self._build, 290 self._patches, 291 tryjob_flags=["--notests"], 292 build_toolchain=True, 293 ) 294 print( 295 "trybot_url: \ 296 http://cros-goldeneye/chromeos/healthmonitoring/buildDetails?buildbucketId=%s" 297 % buildbucket_id 298 ) 299 300 if not trybot_image: 301 self._l.LogError("Unable to find trybot_image!") 302 return 2 303 304 vanilla_image = self._GetVanillaImageName(trybot_image) 305 306 print("trybot_image: %s" % trybot_image) 307 print("vanilla_image: %s" % vanilla_image) 308 309 ret = self._TestImages(trybot_image, vanilla_image) 310 # Always try to send report email as crosperf will generate report when 311 # tests partially succeeded. 312 if not self._test: 313 self._SendEmail() 314 self._CopyJson() 315 # Non-zero ret here means crosperf tests partially failed, raise error here 316 # so that toolchain summary report can catch it. 317 if ret != 0: 318 raise RuntimeError("Crosperf tests partially failed!") 319 320 return 0 321 322 323def Main(argv): 324 """The main function.""" 325 326 # Common initializations 327 command_executer.InitCommandExecuter() 328 parser = argparse.ArgumentParser() 329 parser.add_argument( 330 "--remote", dest="remote", help="Remote machines to run tests on." 331 ) 332 parser.add_argument( 333 "--board", dest="board", default="x86-zgb", help="The target board." 334 ) 335 parser.add_argument( 336 "--chromeos_root", 337 dest="chromeos_root", 338 help="The chromeos root from which to run tests.", 339 ) 340 parser.add_argument( 341 "--weekday", 342 default="", 343 dest="weekday", 344 help="The day of the week for which to run tests.", 345 ) 346 parser.add_argument( 347 "--patch", 348 dest="patches", 349 help="The patches to use for the testing, " 350 "seprate the patch numbers with ',' " 351 "for more than one patches.", 352 ) 353 parser.add_argument( 354 "--noschedv2", 355 dest="noschedv2", 356 action="store_true", 357 default=False, 358 help="Pass --noschedv2 to crosperf.", 359 ) 360 parser.add_argument( 361 "--recipe", 362 dest="recipe", 363 default=True, 364 help="Use images generated from recipe rather than" 365 "launching tryjob to get images.", 366 ) 367 parser.add_argument( 368 "--test", 369 dest="test", 370 default=False, 371 help="Test this script on local desktop, " 372 "disabling mobiletc checking and email sending." 373 "Artifacts stored in /tmp/toolchain-tests", 374 ) 375 376 options = parser.parse_args(argv[1:]) 377 if not options.board: 378 print("Please give a board.") 379 return 1 380 if not options.remote: 381 print("Please give at least one remote machine.") 382 return 1 383 if not options.chromeos_root: 384 print("Please specify the ChromeOS root directory.") 385 return 1 386 if options.test: 387 print("Cleaning local test directory for this script.") 388 if os.path.exists(TMP_TOOLCHAIN_TEST): 389 shutil.rmtree(TMP_TOOLCHAIN_TEST) 390 os.mkdir(TMP_TOOLCHAIN_TEST) 391 392 fc = ToolchainComparator( 393 options.board, 394 options.remote, 395 options.chromeos_root, 396 options.weekday, 397 options.patches, 398 options.recipe, 399 options.test, 400 options.noschedv2, 401 ) 402 return fc.DoAll() 403 404 405if __name__ == "__main__": 406 retval = Main(sys.argv) 407 sys.exit(retval) 408