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