test_toolchains.py revision aee96b71ad4ffde231ca5bf9c0509d15b0e9b753
1#!/usr/bin/python2 2 3# Script to test different toolchains against ChromeOS benchmarks. 4"""Toolchain team nightly performance test script (local builds).""" 5 6from __future__ import print_function 7 8import argparse 9import datetime 10import os 11import sys 12import build_chromeos 13import setup_chromeos 14import time 15from cros_utils import command_executer 16from cros_utils import misc 17from cros_utils import logger 18 19CROSTC_ROOT = '/usr/local/google/crostc' 20MAIL_PROGRAM = '~/var/bin/mail-sheriff' 21WEEKLY_REPORTS_ROOT = os.path.join(CROSTC_ROOT, 'weekly_test_data') 22PENDING_ARCHIVES_DIR = os.path.join(CROSTC_ROOT, 'pending_archives') 23NIGHTLY_TESTS_DIR = os.path.join(CROSTC_ROOT, 'nightly_test_reports') 24 25 26class GCCConfig(object): 27 """GCC configuration class.""" 28 29 def __init__(self, githash): 30 self.githash = githash 31 32 33class ToolchainConfig(object): 34 """Toolchain configuration class.""" 35 36 def __init__(self, gcc_config=None): 37 self.gcc_config = gcc_config 38 39 40class ChromeOSCheckout(object): 41 """Main class for checking out, building and testing ChromeOS.""" 42 43 def __init__(self, board, chromeos_root): 44 self._board = board 45 self._chromeos_root = chromeos_root 46 self._ce = command_executer.GetCommandExecuter() 47 self._l = logger.GetLogger() 48 self._build_num = None 49 50 def _DeleteChroot(self): 51 command = 'cd %s; cros_sdk --delete' % self._chromeos_root 52 return self._ce.RunCommand(command) 53 54 def _DeleteCcahe(self): 55 # crosbug.com/34956 56 command = 'sudo rm -rf %s' % os.path.join(self._chromeos_root, '.cache') 57 return self._ce.RunCommand(command) 58 59 def _GetBuildNumber(self): 60 """Get the build number of the ChromeOS image from the chroot. 61 62 This function assumes a ChromeOS image has been built in the chroot. 63 It translates the 'latest' symlink in the 64 <chroot>/src/build/images/<board> directory, to find the actual 65 ChromeOS build number for the image that was built. For example, if 66 src/build/image/lumpy/latest -> R37-5982.0.2014_06_23_0454-a1, then 67 This function would parse it out and assign 'R37-5982' to self._build_num. 68 This is used to determine the official, vanilla build to use for 69 comparison tests. 70 """ 71 # Get the path to 'latest' 72 sym_path = os.path.join( 73 misc.GetImageDir(self._chromeos_root, self._board), 'latest') 74 # Translate the symlink to its 'real' path. 75 real_path = os.path.realpath(sym_path) 76 # Break up the path and get the last piece 77 # (e.g. 'R37-5982.0.2014_06_23_0454-a1" 78 path_pieces = real_path.split('/') 79 last_piece = path_pieces[-1] 80 # Break this piece into the image number + other pieces, and get the 81 # image number [ 'R37-5982', '0', '2014_06_23_0454-a1'] 82 image_parts = last_piece.split('.') 83 self._build_num = image_parts[0] 84 85 def _BuildLabelName(self, config): 86 pieces = config.split('/') 87 compiler_version = pieces[-1] 88 label = compiler_version + '_tot_afdo' 89 return label 90 91 def _BuildAndImage(self, label=''): 92 if (not label or 93 not misc.DoesLabelExist(self._chromeos_root, self._board, label)): 94 build_chromeos_args = [ 95 build_chromeos.__file__, '--chromeos_root=%s' % self._chromeos_root, 96 '--board=%s' % self._board, '--rebuild' 97 ] 98 if self._public: 99 build_chromeos_args.append('--env=USE=-chrome_internal') 100 101 ret = build_chromeos.Main(build_chromeos_args) 102 if ret != 0: 103 raise RuntimeError("Couldn't build ChromeOS!") 104 105 if not self._build_num: 106 self._GetBuildNumber() 107 # Check to see if we need to create the symbolic link for the vanilla 108 # image, and do so if appropriate. 109 if not misc.DoesLabelExist(self._chromeos_root, self._board, 'vanilla'): 110 build_name = '%s-release/%s.0.0' % (self._board, self._build_num) 111 full_vanilla_path = os.path.join(os.getcwd(), self._chromeos_root, 112 'chroot/tmp', build_name) 113 misc.LabelLatestImage(self._chromeos_root, self._board, label, 114 full_vanilla_path) 115 else: 116 misc.LabelLatestImage(self._chromeos_root, self._board, label) 117 return label 118 119 def _SetupBoard(self, env_dict, usepkg_flag, clobber_flag): 120 env_string = misc.GetEnvStringFromDict(env_dict) 121 command = ('%s %s' % (env_string, misc.GetSetupBoardCommand( 122 self._board, usepkg=usepkg_flag, force=clobber_flag))) 123 ret = self._ce.ChrootRunCommand(self._chromeos_root, command) 124 error_str = "Could not setup board: '%s'" % command 125 assert ret == 0, error_str 126 127 def _UnInstallToolchain(self): 128 command = ('sudo CLEAN_DELAY=0 emerge -C cross-%s/gcc' % 129 misc.GetCtargetFromBoard(self._board, self._chromeos_root)) 130 ret = self._ce.ChrootRunCommand(self._chromeos_root, command) 131 if ret != 0: 132 raise RuntimeError("Couldn't uninstall the toolchain!") 133 134 def _CheckoutChromeOS(self): 135 # TODO(asharif): Setup a fixed ChromeOS version (quarterly snapshot). 136 if not os.path.exists(self._chromeos_root): 137 setup_chromeos_args = ['--dir=%s' % self._chromeos_root] 138 if self._public: 139 setup_chromeos_args.append('--public') 140 ret = setup_chromeos.Main(setup_chromeos_args) 141 if ret != 0: 142 raise RuntimeError("Couldn't run setup_chromeos!") 143 144 def _BuildToolchain(self, config): 145 # Call setup_board for basic, vanilla setup. 146 self._SetupBoard({}, usepkg_flag=True, clobber_flag=False) 147 # Now uninstall the vanilla compiler and setup/build our custom 148 # compiler. 149 self._UnInstallToolchain() 150 envdict = { 151 'USE': 'git_gcc', 152 'GCC_GITHASH': config.gcc_config.githash, 153 'EMERGE_DEFAULT_OPTS': '--exclude=gcc' 154 } 155 self._SetupBoard(envdict, usepkg_flag=False, clobber_flag=False) 156 157 158class ToolchainComparator(ChromeOSCheckout): 159 """Main class for running tests and generating reports.""" 160 161 def __init__(self, 162 board, 163 remotes, 164 configs, 165 clean, 166 public, 167 force_mismatch, 168 noschedv2=False): 169 self._board = board 170 self._remotes = remotes 171 self._chromeos_root = 'chromeos' 172 self._configs = configs 173 self._clean = clean 174 self._public = public 175 self._force_mismatch = force_mismatch 176 self._ce = command_executer.GetCommandExecuter() 177 self._l = logger.GetLogger() 178 timestamp = datetime.datetime.strftime(datetime.datetime.now(), 179 '%Y-%m-%d_%H:%M:%S') 180 self._reports_dir = os.path.join( 181 NIGHTLY_TESTS_DIR, 182 '%s.%s' % (timestamp, board),) 183 self._noschedv2 = noschedv2 184 ChromeOSCheckout.__init__(self, board, self._chromeos_root) 185 186 def _FinishSetup(self): 187 # Get correct .boto file 188 current_dir = os.getcwd() 189 src = '/usr/local/google/home/mobiletc-prebuild/.boto' 190 dest = os.path.join(current_dir, self._chromeos_root, 191 'src/private-overlays/chromeos-overlay/' 192 'googlestorage_account.boto') 193 # Copy the file to the correct place 194 copy_cmd = 'cp %s %s' % (src, dest) 195 retv = self._ce.RunCommand(copy_cmd) 196 if retv != 0: 197 raise RuntimeError("Couldn't copy .boto file for google storage.") 198 199 # Fix protections on ssh key 200 command = ('chmod 600 /var/cache/chromeos-cache/distfiles/target' 201 '/chrome-src-internal/src/third_party/chromite/ssh_keys' 202 '/testing_rsa') 203 retv = self._ce.ChrootRunCommand(self._chromeos_root, command) 204 if retv != 0: 205 raise RuntimeError('chmod for testing_rsa failed') 206 207 def _TestLabels(self, labels): 208 experiment_file = 'toolchain_experiment.txt' 209 image_args = '' 210 if self._force_mismatch: 211 image_args = '--force-mismatch' 212 experiment_header = """ 213 board: %s 214 remote: %s 215 retries: 1 216 """ % (self._board, self._remotes) 217 experiment_tests = """ 218 benchmark: all_toolchain_perf { 219 suite: telemetry_Crosperf 220 iterations: 3 221 } 222 """ 223 224 with open(experiment_file, 'w') as f: 225 f.write(experiment_header) 226 f.write(experiment_tests) 227 for label in labels: 228 # TODO(asharif): Fix crosperf so it accepts labels with symbols 229 crosperf_label = label 230 crosperf_label = crosperf_label.replace('-', '_') 231 crosperf_label = crosperf_label.replace('+', '_') 232 crosperf_label = crosperf_label.replace('.', '') 233 234 # Use the official build instead of building vanilla ourselves. 235 if label == 'vanilla': 236 build_name = '%s-release/%s.0.0' % (self._board, self._build_num) 237 238 # Now add 'official build' to test file. 239 official_image = """ 240 official_image { 241 chromeos_root: %s 242 build: %s 243 } 244 """ % (self._chromeos_root, build_name) 245 f.write(official_image) 246 247 else: 248 experiment_image = """ 249 %s { 250 chromeos_image: %s 251 image_args: %s 252 } 253 """ % (crosperf_label, os.path.join( 254 misc.GetImageDir(self._chromeos_root, self._board), label, 255 'chromiumos_test_image.bin'), image_args) 256 f.write(experiment_image) 257 258 crosperf = os.path.join(os.path.dirname(__file__), 'crosperf', 'crosperf') 259 noschedv2_opts = '--noschedv2' if self._noschedv2 else '' 260 command = ('{crosperf} --no_email=True --results_dir={r_dir} ' 261 '--json_report=True {noschedv2_opts} {exp_file}').format( 262 crosperf=crosperf, 263 r_dir=self._reports_dir, 264 noschedv2_opts=noschedv2_opts, 265 exp_file=experiment_file) 266 267 ret = self._ce.RunCommand(command) 268 if ret != 0: 269 raise RuntimeError('Crosperf execution error!') 270 else: 271 # Copy json report to pending archives directory. 272 command = 'cp %s/*.json %s/.' % (self._reports_dir, PENDING_ARCHIVES_DIR) 273 ret = self._ce.RunCommand(command) 274 return 275 276 def _CopyWeeklyReportFiles(self, labels): 277 """Move files into place for creating 7-day reports. 278 279 Create tar files of the custom and official images and copy them 280 to the weekly reports directory, so they exist when the weekly report 281 gets generated. IMPORTANT NOTE: This function must run *after* 282 crosperf has been run; otherwise the vanilla images will not be there. 283 """ 284 images_path = os.path.join( 285 os.path.realpath(self._chromeos_root), 'src/build/images', self._board) 286 weekday = time.strftime('%a') 287 data_dir = os.path.join(WEEKLY_REPORTS_ROOT, self._board) 288 dest_dir = os.path.join(data_dir, weekday) 289 if not os.path.exists(dest_dir): 290 os.makedirs(dest_dir) 291 # Make sure dest_dir is empty (clean out last week's data). 292 cmd = 'cd %s; rm -Rf %s_*_image*' % (dest_dir, weekday) 293 self._ce.RunCommand(cmd) 294 # Now create new tar files and copy them over. 295 for l in labels: 296 test_path = os.path.join(images_path, l) 297 if os.path.exists(test_path): 298 if l != 'vanilla': 299 label_name = 'test' 300 else: 301 label_name = 'vanilla' 302 tar_file_name = '%s_%s_image.tar' % (weekday, label_name) 303 cmd = ('cd %s; tar -cvf %s %s/chromiumos_test_image.bin; ' 304 'cp %s %s/.') % (images_path, tar_file_name, l, tar_file_name, 305 dest_dir) 306 tar_ret = self._ce.RunCommand(cmd) 307 if tar_ret != 0: 308 self._l.LogOutput('Error while creating/copying test tar file(%s).' % 309 tar_file_name) 310 311 def _SendEmail(self): 312 """Find email msesage generated by crosperf and send it.""" 313 filename = os.path.join(self._reports_dir, 'msg_body.html') 314 if (os.path.exists(filename) and 315 os.path.exists(os.path.expanduser(MAIL_PROGRAM))): 316 command = ('cat %s | %s -s "Nightly test results, %s" -team -html' % 317 (filename, MAIL_PROGRAM, self._board)) 318 self._ce.RunCommand(command) 319 320 def DoAll(self): 321 self._CheckoutChromeOS() 322 labels = [] 323 labels.append('vanilla') 324 for config in self._configs: 325 label = self._BuildLabelName(config.gcc_config.githash) 326 if not misc.DoesLabelExist(self._chromeos_root, self._board, label): 327 self._BuildToolchain(config) 328 label = self._BuildAndImage(label) 329 labels.append(label) 330 self._FinishSetup() 331 self._TestLabels(labels) 332 self._SendEmail() 333 # Only try to copy the image files if the test runs ran successfully. 334 self._CopyWeeklyReportFiles(labels) 335 if self._clean: 336 ret = self._DeleteChroot() 337 if ret != 0: 338 return ret 339 ret = self._DeleteCcahe() 340 if ret != 0: 341 return ret 342 return 0 343 344 345def Main(argv): 346 """The main function.""" 347 # Common initializations 348 ### command_executer.InitCommandExecuter(True) 349 command_executer.InitCommandExecuter() 350 parser = argparse.ArgumentParser() 351 parser.add_argument( 352 '--remote', dest='remote', help='Remote machines to run tests on.') 353 parser.add_argument( 354 '--board', dest='board', default='x86-alex', help='The target board.') 355 parser.add_argument( 356 '--githashes', 357 dest='githashes', 358 default='master', 359 help='The gcc githashes to test.') 360 parser.add_argument( 361 '--clean', 362 dest='clean', 363 default=False, 364 action='store_true', 365 help='Clean the chroot after testing.') 366 parser.add_argument( 367 '--public', 368 dest='public', 369 default=False, 370 action='store_true', 371 help='Use the public checkout/build.') 372 parser.add_argument( 373 '--force-mismatch', 374 dest='force_mismatch', 375 default='', 376 help='Force the image regardless of board mismatch') 377 parser.add_argument( 378 '--noschedv2', 379 dest='noschedv2', 380 action='store_true', 381 default=False, 382 help='Pass --noschedv2 to crosperf.') 383 options = parser.parse_args(argv) 384 if not options.board: 385 print('Please give a board.') 386 return 1 387 if not options.remote: 388 print('Please give at least one remote machine.') 389 return 1 390 toolchain_configs = [] 391 for githash in options.githashes.split(','): 392 gcc_config = GCCConfig(githash=githash) 393 toolchain_config = ToolchainConfig(gcc_config=gcc_config) 394 toolchain_configs.append(toolchain_config) 395 fc = ToolchainComparator(options.board, options.remote, toolchain_configs, 396 options.clean, options.public, 397 options.force_mismatch, options.noschedv2) 398 return fc.DoAll() 399 400 401if __name__ == '__main__': 402 retval = Main(sys.argv[1:]) 403 sys.exit(retval) 404