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