results_cache.py revision b47bff4d3336c5fe5593a95963c0f3dc20a02f68
1#!/usr/bin/python 2 3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7import getpass 8import glob 9import hashlib 10import os 11import pickle 12import re 13import tempfile 14 15from utils import command_executer 16from utils import misc 17 18from image_checksummer import ImageChecksummer 19 20SCRATCH_BASE = "/home/%s/cros_scratch" 21SCRATCH_DIR = SCRATCH_BASE % getpass.getuser() 22RESULTS_FILE = "results.txt" 23MACHINE_FILE = "machine.txt" 24AUTOTEST_TARBALL = "autotest.tbz2" 25PERF_RESULTS_FILE = "perf-results.txt" 26 27 28class Result(object): 29 """ This class manages what exactly is stored inside the cache without knowing 30 what the key of the cache is. For runs with perf, it stores perf.data, 31 perf.report, etc. The key generation is handled by the ResultsCache class. 32 """ 33 34 def __init__(self, logger, label): 35 self._chromeos_root = label.chromeos_root 36 self._logger = logger 37 self._ce = command_executer.GetCommandExecuter(self._logger) 38 self._temp_dir = None 39 self.label = label 40 self.results_dir = None 41 self.perf_data_files = [] 42 self.perf_report_files = [] 43 44 def _CopyFilesTo(self, dest_dir, files_to_copy): 45 file_index = 0 46 for file_to_copy in files_to_copy: 47 if not os.path.isdir(dest_dir): 48 command = "mkdir -p %s" % dest_dir 49 self._ce.RunCommand(command) 50 dest_file = os.path.join(dest_dir, 51 ("%s.%s" % (os.path.basename(file_to_copy), 52 file_index))) 53 ret = self._ce.CopyFiles(file_to_copy, 54 dest_file, 55 recursive=False) 56 if ret: 57 raise Exception("Could not copy results file: %s" % file_to_copy) 58 59 def CopyResultsTo(self, dest_dir): 60 self._CopyFilesTo(dest_dir, self.perf_data_files) 61 self._CopyFilesTo(dest_dir, self.perf_report_files) 62 63 def _GetKeyvals(self): 64 results_in_chroot = os.path.join(self._chromeos_root, 65 "chroot", "tmp") 66 if not self._temp_dir: 67 self._temp_dir = tempfile.mkdtemp(dir=results_in_chroot) 68 command = "cp -r {0}/* {1}".format(self.results_dir, self._temp_dir) 69 self._ce.RunCommand(command) 70 71 command = ("python generate_test_report --no-color --csv %s" % 72 (os.path.join("/tmp", os.path.basename(self._temp_dir)))) 73 [_, out, _] = self._ce.ChrootRunCommand(self._chromeos_root, 74 command, 75 return_output=True) 76 keyvals_dict = {} 77 tmp_dir_in_chroot = misc.GetInsideChrootPath(self._chromeos_root, 78 self._temp_dir) 79 for line in out.splitlines(): 80 tokens = re.split("=|,", line) 81 key = tokens[-2] 82 if key.startswith(tmp_dir_in_chroot): 83 key = key[len(tmp_dir_in_chroot) + 1:] 84 value = tokens[-1] 85 keyvals_dict[key] = value 86 87 return keyvals_dict 88 89 def _GetResultsDir(self): 90 mo = re.search(r"Results placed in (\S+)", self.out) 91 if mo: 92 result = mo.group(1) 93 return result 94 raise Exception("Could not find results directory.") 95 96 def _FindFilesInResultsDir(self, find_args): 97 if not self.results_dir: 98 return None 99 100 command = "find %s %s" % (self.results_dir, 101 find_args) 102 ret, out, _ = self._ce.RunCommand(command, return_output=True) 103 if ret: 104 raise Exception("Could not run find command!") 105 return out 106 107 def _GetPerfDataFiles(self): 108 return self._FindFilesInResultsDir("-name perf.data").splitlines() 109 110 def _GetPerfReportFiles(self): 111 return self._FindFilesInResultsDir("-name perf.data.report").splitlines() 112 113 def _GeneratePerfReportFiles(self): 114 perf_report_files = [] 115 for perf_data_file in self.perf_data_files: 116 # Generate a perf.report and store it side-by-side with the perf.data 117 # file. 118 chroot_perf_data_file = misc.GetInsideChrootPath(self._chromeos_root, 119 perf_data_file) 120 perf_report_file = "%s.report" % perf_data_file 121 if os.path.exists(perf_report_file): 122 raise Exception("Perf report file already exists: %s" % 123 perf_report_file) 124 chroot_perf_report_file = misc.GetInsideChrootPath(self._chromeos_root, 125 perf_report_file) 126 command = ("/usr/sbin/perf report " 127 "-n " 128 "--symfs /build/%s " 129 "--vmlinux /build/%s/usr/lib/debug/boot/vmlinux " 130 "--kallsyms /build/%s/boot/System.map-* " 131 "-i %s --stdio " 132 "> %s" % 133 (self._board, 134 self._board, 135 self._board, 136 chroot_perf_data_file, 137 chroot_perf_report_file)) 138 self._ce.ChrootRunCommand(self._chromeos_root, 139 command) 140 141 # Add a keyval to the dictionary for the events captured. 142 perf_report_files.append( 143 misc.GetOutsideChrootPath(self._chromeos_root, 144 chroot_perf_report_file)) 145 return perf_report_files 146 147 def _GatherPerfResults(self): 148 report_id = 0 149 for perf_report_file in self.perf_report_files: 150 with open(perf_report_file, "r") as f: 151 report_contents = f.read() 152 for group in re.findall(r"Events: (\S+) (\S+)", report_contents): 153 num_events = group[0] 154 event_name = group[1] 155 key = "perf_%s_%s" % (report_id, event_name) 156 value = str(misc.UnitToNumber(num_events)) 157 self.keyvals[key] = value 158 159 def _PopulateFromRun(self, out, err, retval): 160 self._board = self.label.board 161 self.out = out 162 self.err = err 163 self.retval = retval 164 self.chroot_results_dir = self._GetResultsDir() 165 self.results_dir = misc.GetOutsideChrootPath(self._chromeos_root, 166 self.chroot_results_dir) 167 self.perf_data_files = self._GetPerfDataFiles() 168 # Include all perf.report data in table. 169 self.perf_report_files = self._GeneratePerfReportFiles() 170 # TODO(asharif): Do something similar with perf stat. 171 172 # Grab keyvals from the directory. 173 self._ProcessResults() 174 175 def _ProcessResults(self): 176 # Note that this function doesn't know anything about whether there is a 177 # cache hit or miss. It should process results agnostic of the cache hit 178 # state. 179 self.keyvals = self._GetKeyvals() 180 self.keyvals["retval"] = self.retval 181 # Generate report from all perf.data files. 182 # Now parse all perf report files and include them in keyvals. 183 self._GatherPerfResults() 184 185 def _PopulateFromCacheDir(self, cache_dir): 186 # Read in everything from the cache directory. 187 with open(os.path.join(cache_dir, RESULTS_FILE), "r") as f: 188 self.out = pickle.load(f) 189 self.err = pickle.load(f) 190 self.retval = pickle.load(f) 191 192 # Untar the tarball to a temporary directory 193 self._temp_dir = tempfile.mkdtemp(dir=os.path.join(self._chromeos_root, 194 "chroot", "tmp")) 195 196 command = ("cd %s && tar xf %s" % 197 (self._temp_dir, 198 os.path.join(cache_dir, AUTOTEST_TARBALL))) 199 ret = self._ce.RunCommand(command) 200 if ret: 201 raise Exception("Could not untar cached tarball") 202 self.results_dir = self._temp_dir 203 self.perf_data_files = self._GetPerfDataFiles() 204 self.perf_report_files = self._GetPerfReportFiles() 205 self._ProcessResults() 206 207 def CleanUp(self, rm_chroot_tmp): 208 if rm_chroot_tmp and self.results_dir: 209 command = "rm -rf %s" % self.results_dir 210 self._ce.RunCommand(command) 211 if self._temp_dir: 212 command = "rm -rf %s" % self._temp_dir 213 self._ce.RunCommand(command) 214 215 def StoreToCacheDir(self, cache_dir, machine_manager): 216 # Create the dir if it doesn't exist. 217 temp_dir = tempfile.mkdtemp() 218 219 # Store to the temp directory. 220 with open(os.path.join(temp_dir, RESULTS_FILE), "w") as f: 221 pickle.dump(self.out, f) 222 pickle.dump(self.err, f) 223 pickle.dump(self.retval, f) 224 225 if self.results_dir: 226 tarball = os.path.join(temp_dir, AUTOTEST_TARBALL) 227 command = ("cd %s && " 228 "tar " 229 "--exclude=var/spool " 230 "--exclude=var/log " 231 "-cjf %s ." % (self.results_dir, tarball)) 232 ret = self._ce.RunCommand(command) 233 if ret: 234 raise Exception("Couldn't store autotest output directory.") 235 # Store machine info. 236 # TODO(asharif): Make machine_manager a singleton, and don't pass it into 237 # this function. 238 with open(os.path.join(temp_dir, MACHINE_FILE), "w") as f: 239 f.write(machine_manager.machine_checksum_string[self.label.name]) 240 241 if os.path.exists(cache_dir): 242 command = "rm -rf {0}".format(cache_dir) 243 self._ce.RunCommand(command) 244 245 command = "mkdir -p {0} && ".format(os.path.dirname(cache_dir)) 246 command += "chmod g+x {0} && ".format(temp_dir) 247 command += "mv {0} {1}".format(temp_dir, cache_dir) 248 ret = self._ce.RunCommand(command) 249 if ret: 250 command = "rm -rf {0}".format(temp_dir) 251 self._ce.RunCommand(command) 252 raise Exception("Could not move dir %s to dir %s" % 253 (temp_dir, cache_dir)) 254 255 @classmethod 256 def CreateFromRun(cls, logger, label, out, err, retval, suite="pyauto"): 257 if suite == "telemetry": 258 result = TelemetryResult(logger, label) 259 else: 260 result = cls(logger, label) 261 result._PopulateFromRun(out, err, retval) 262 return result 263 264 @classmethod 265 def CreateFromCacheHit(cls, logger, label, cache_dir, 266 suite="pyauto"): 267 if suite == "telemetry": 268 result = TelemetryResult(logger, label) 269 else: 270 result = cls(logger, label) 271 try: 272 result._PopulateFromCacheDir(cache_dir) 273 274 except Exception as e: 275 logger.LogError("Exception while using cache: %s" % e) 276 return None 277 return result 278 279class TelemetryResult(Result): 280 281 def __init__(self, logger, label): 282 super(TelemetryResult, self).__init__(logger, label) 283 284 def _PopulateFromRun(self, out, err, retval): 285 self.out = out 286 self.err = err 287 self.retval = retval 288 289 self._ProcessResults() 290 291 def _ProcessResults(self): 292 # The output is: 293 # url,average_commit_time (ms),... 294 # www.google.com,33.4,21.2,... 295 # We need to convert to this format: 296 # {"www.google.com:average_commit_time (ms)": "33.4", 297 # "www.google.com:...": "21.2"} 298 # Added note: Occasionally the output comes back 299 # with "JSON.stringify(window.automation.GetResults())" on 300 # the first line, and then the rest of the output as 301 # described above. 302 303 lines = self.out.splitlines() 304 self.keyvals = {} 305 306 if lines: 307 if lines[0].startswith("JSON.stringify"): 308 lines = lines[1:] 309 310 if not lines: 311 return 312 labels = lines[0].split(",") 313 for line in lines[1:]: 314 fields = line.split(",") 315 if (len(fields) != len(labels)): 316 continue 317 for i in range(1, len(labels)): 318 key = "%s %s" % (fields[0], labels[i]) 319 value = fields[i] 320 self.keyvals[key] = value 321 self.keyvals["retval"] = self.retval 322 323 def _PopulateFromCacheDir(self, cache_dir): 324 with open(os.path.join(cache_dir, RESULTS_FILE), "r") as f: 325 self.out = pickle.load(f) 326 self.err = pickle.load(f) 327 self.retval = pickle.load(f) 328 self._ProcessResults() 329 330 331class CacheConditions(object): 332 # Cache hit only if the result file exists. 333 CACHE_FILE_EXISTS = 0 334 335 # Cache hit if the checksum of cpuinfo and totalmem of 336 # the cached result and the new run match. 337 MACHINES_MATCH = 1 338 339 # Cache hit if the image checksum of the cached result and the new run match. 340 CHECKSUMS_MATCH = 2 341 342 # Cache hit only if the cached result was successful 343 RUN_SUCCEEDED = 3 344 345 # Never a cache hit. 346 FALSE = 4 347 348 # Cache hit if the image path matches the cached image path. 349 IMAGE_PATH_MATCH = 5 350 351 # Cache hit if the uuid of hard disk mataches the cached one 352 353 SAME_MACHINE_MATCH = 6 354 355 356class ResultsCache(object): 357 """ This class manages the key of the cached runs without worrying about what 358 is exactly stored (value). The value generation is handled by the Results 359 class. 360 """ 361 CACHE_VERSION = 6 362 363 def Init(self, chromeos_image, chromeos_root, test_name, iteration, 364 test_args, machine_manager, board, cache_conditions, 365 logger_to_use, label, share_users, suite): 366 self.chromeos_image = chromeos_image 367 self.chromeos_root = chromeos_root 368 self.test_name = test_name 369 self.iteration = iteration 370 self.test_args = test_args, 371 self.board = board 372 self.cache_conditions = cache_conditions 373 self.machine_manager = machine_manager 374 self._logger = logger_to_use 375 self._ce = command_executer.GetCommandExecuter(self._logger) 376 self.label = label 377 self.share_users = share_users 378 self.suite = suite 379 380 def _GetCacheDirForRead(self): 381 matching_dirs = [] 382 for glob_path in self._FormCacheDir(self._GetCacheKeyList(True)): 383 matching_dirs += glob.glob(glob_path) 384 385 if matching_dirs: 386 # Cache file found. 387 return matching_dirs[0] 388 else: 389 return None 390 391 def _GetCacheDirForWrite(self): 392 return self._FormCacheDir(self._GetCacheKeyList(False))[0] 393 394 def _FormCacheDir(self, list_of_strings): 395 cache_key = " ".join(list_of_strings) 396 cache_dir = misc.GetFilenameFromString(cache_key) 397 if self.label.cache_dir: 398 cache_home = os.path.abspath(os.path.expanduser(self.label.cache_dir)) 399 cache_path = [os.path.join(cache_home, cache_dir)] 400 else: 401 cache_path = [os.path.join(SCRATCH_DIR, cache_dir)] 402 403 for i in [x.strip() for x in self.share_users.split(",")]: 404 path = SCRATCH_BASE % i 405 cache_path.append(os.path.join(path, cache_dir)) 406 407 return cache_path 408 409 def _GetCacheKeyList(self, read): 410 if read and CacheConditions.MACHINES_MATCH not in self.cache_conditions: 411 machine_checksum = "*" 412 else: 413 machine_checksum = self.machine_manager.machine_checksum[self.label.name] 414 if read and CacheConditions.CHECKSUMS_MATCH not in self.cache_conditions: 415 checksum = "*" 416 else: 417 checksum = ImageChecksummer().Checksum(self.label) 418 419 if read and CacheConditions.IMAGE_PATH_MATCH not in self.cache_conditions: 420 image_path_checksum = "*" 421 else: 422 image_path_checksum = hashlib.md5(self.chromeos_image).hexdigest() 423 424 machine_id_checksum = "" 425 if read and CacheConditions.SAME_MACHINE_MATCH not in self.cache_conditions: 426 machine_id_checksum = "*" 427 else: 428 for machine in self.machine_manager.GetMachines(self.label): 429 if machine.name == self.label.remote[0]: 430 machine_id_checksum = machine.machine_id_checksum 431 break 432 433 test_args_checksum = hashlib.md5( 434 "".join(self.test_args)).hexdigest() 435 return (image_path_checksum, 436 self.test_name, str(self.iteration), 437 test_args_checksum, 438 checksum, 439 machine_checksum, 440 machine_id_checksum, 441 str(self.CACHE_VERSION)) 442 443 def ReadResult(self): 444 if CacheConditions.FALSE in self.cache_conditions: 445 return None 446 cache_dir = self._GetCacheDirForRead() 447 448 if not cache_dir: 449 return None 450 451 if not os.path.isdir(cache_dir): 452 return None 453 454 self._logger.LogOutput("Trying to read from cache dir: %s" % cache_dir) 455 result = Result.CreateFromCacheHit(self._logger, 456 self.label, 457 cache_dir, 458 self.suite) 459 if not result: 460 return None 461 462 if (result.retval == 0 or 463 CacheConditions.RUN_SUCCEEDED not in self.cache_conditions): 464 return result 465 466 return None 467 468 def StoreResult(self, result): 469 cache_dir = self._GetCacheDirForWrite() 470 result.StoreToCacheDir(cache_dir, self.machine_manager) 471 472 473class MockResultsCache(ResultsCache): 474 def Init(self, *args): 475 pass 476 477 def ReadResult(self): 478 return None 479 480 def StoreResult(self, result): 481 pass 482 483 484class MockResult(Result): 485 def _PopulateFromRun(self, out, err, retval): 486 self.out = out 487 self.err = err 488 self.retval = retval 489 490