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