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