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