results_cache.py revision f84bd3b7b64deba46192ae6d15a712ab66e90a6e
1#!/usr/bin/python
2#
3# Copyright 2011 Google Inc. All Rights Reserved.
4
5import getpass
6import glob
7import hashlib
8import os
9import pickle
10import re
11import tempfile
12
13from image_checksummer import ImageChecksummer
14from utils import command_executer
15from utils import logger
16from utils import misc
17
18
19SCRATCH_DIR = "/home/%s/cros_scratch" % getpass.getuser()
20RESULTS_FILE = "results.txt"
21AUTOTEST_TARBALL = "autotest.tbz2"
22PERF_RESULTS_FILE = "perf-results.txt"
23
24
25class Result(object):
26  """ This class manages what exactly is stored inside the cache without knowing
27  what the key of the cache is. For runs with perf, it stores perf.data,
28  perf.report, etc. The key generation is handled by the ResultsCache class.
29  """
30  def __init__(self, logger):
31    self._logger = logger
32    self._ce = command_executer.GetCommandExecuter(self._logger)
33    self._temp_dir = None
34
35  def _CopyFilesTo(self, dest_dir, files_to_copy):
36    file_index = 0
37    for file_to_copy in files_to_copy:
38      if not os.path.isdir(dest_dir):
39        command = "mkdir -p %s" % dest_dir
40        self._ce.RunCommand(command)
41      dest_file = os.path.join(dest_dir,
42                               ("%s.%s" % (os.path.basename(file_to_copy),
43                                           file_index)))
44      ret = self._ce.CopyFiles(file_to_copy,
45                               dest_file,
46                               recursive=False)
47      if ret:
48        raise Exception("Could not copy results file: %s" % file_to_copy)
49
50  def CopyResultsTo(self, dest_dir):
51    self._CopyFilesTo(dest_dir, self.perf_data_files)
52    self._CopyFilesTo(dest_dir, self.perf_report_files)
53
54  def _GetKeyvals(self):
55    command = "find %s -regex .*results/keyval$" % self.results_dir
56    [ret, out, err] = self._ce.RunCommand(command, return_output=True)
57    keyvals_dict = {}
58    for f in out.splitlines():
59      keyvals = open(f, "r").read()
60      keyvals_dict.update(self._ParseKeyvals(keyvals))
61
62    return keyvals_dict
63
64  def _ParseKeyvals(self, keyvals):
65    keyval_dict = {}
66    for l in keyvals.splitlines():
67      l = l.strip()
68      if l:
69        key, val = l.split("=")
70        keyval_dict[key] = val
71    return keyval_dict
72
73  def _GetResultsDir(self):
74    mo = re.search("Results placed in (\S+)", self.out)
75    if mo:
76      result = mo.group(1)
77      return result
78    raise Exception("Could not find results directory.")
79
80  def _FindFilesInResultsDir(self, find_args):
81    command = "find %s %s" % (self.results_dir,
82                              find_args)
83    ret, out, err = self._ce.RunCommand(command, return_output=True)
84    if ret:
85      raise Exception("Could not run find command!")
86    return out
87
88  def _GetPerfDataFiles(self):
89    return self._FindFilesInResultsDir("-name perf.data").splitlines()
90
91  def _GetPerfReportFiles(self):
92    return self._FindFilesInResultsDir("-name perf.data.report").splitlines()
93
94  def _GeneratePerfReportFiles(self):
95    perf_report_files = []
96    for perf_data_file in self.perf_data_files:
97      # Generate a perf.report and store it side-by-side with the perf.data
98      # file.
99      chroot_perf_data_file = misc.GetInsideChrootPath(self._chromeos_root,
100                                                       perf_data_file)
101      perf_report_file = "%s.report" % perf_data_file
102      if os.path.exists(perf_report_file):
103        raise Exception("Perf report file already exists: %s" %
104                        perf_report_file)
105      chroot_perf_report_file = misc.GetInsideChrootPath(self._chromeos_root,
106                                                   perf_report_file)
107      command = ("/usr/sbin/perf report "
108                 "--symfs /build/%s "
109                 "--vmlinux /build/%s/usr/lib/debug/boot/vmlinux "
110                 "--kallsyms /build/%s/boot/System.map-* "
111                 "-i %s --stdio "
112                 "| head -n1000 "
113                 "| tee %s" %
114                 (self._board,
115                  self._board,
116                  self._board,
117                  chroot_perf_data_file,
118                  chroot_perf_report_file))
119      ret, out, err = self._ce.ChrootRunCommand(self._chromeos_root,
120                                                command,
121                                                return_output=True)
122      # Add a keyval to the dictionary for the events captured.
123      perf_report_files.append(
124          misc.GetOutsideChrootPath(self._chromeos_root,
125                                    chroot_perf_report_file))
126    return perf_report_files
127
128  def _GatherPerfResults(self):
129    report_id = 0
130    for perf_report_file in self.perf_report_files:
131      with open(perf_report_file, "r") as f:
132        report_contents = f.read()
133        for group in re.findall("Events: (\S+) (\S+)", report_contents):
134          num_events = group[0]
135          event_name = group[1]
136          key = "perf_%s_%s" % (report_id, event_name)
137          value = str(misc.UnitToNumber(num_events))
138          self.keyvals[key] = value
139
140  def _PopulateFromRun(self, chromeos_root, board, out, err, retval):
141    self._chromeos_root = chromeos_root
142    self._board = board
143    self.out = out
144    self.err = err
145    self.retval = retval
146    self.chroot_results_dir = self._GetResultsDir()
147    self.results_dir = misc.GetOutsideChrootPath(self._chromeos_root,
148                                                 self.chroot_results_dir)
149    self.perf_data_files = self._GetPerfDataFiles()
150    # Include all perf.report data in table.
151    self.perf_report_files = self._GeneratePerfReportFiles()
152    # TODO(asharif): Do something similar with perf stat.
153
154    # Grab keyvals from the directory.
155    self._ProcessResults()
156
157  def _ProcessResults(self):
158    # Note that this function doesn't know anything about whether there is a
159    # cache hit or miss. It should process results agnostic of the cache hit
160    # state.
161    self.keyvals = self._GetKeyvals()
162    self.keyvals["retval"] = self.retval
163    # Generate report from all perf.data files.
164    # Now parse all perf report files and include them in keyvals.
165    self._GatherPerfResults()
166
167  def _PopulateFromCacheDir(self, cache_dir):
168    # Read in everything from the cache directory.
169    with open(os.path.join(cache_dir, RESULTS_FILE), "r") as f:
170      self.out = pickle.load(f)
171      self.err = pickle.load(f)
172      self.retval = pickle.load(f)
173
174    # Untar the tarball to a temporary directory
175    self._temp_dir = tempfile.mkdtemp()
176    command = ("cd %s && tar xf %s" %
177               (self._temp_dir,
178                os.path.join(cache_dir, AUTOTEST_TARBALL)))
179    ret = self._ce.RunCommand(command)
180    if ret:
181      raise Exception("Could not untar cached tarball")
182    self.results_dir = self._temp_dir
183    self.perf_data_files = self._GetPerfDataFiles()
184    self.perf_report_files = self._GetPerfReportFiles()
185    self._ProcessResults()
186
187  def CleanUp(self):
188    if self._temp_dir:
189      command = "rm -rf %s" % self._temp_dir
190      self._ce.RunCommand(command)
191
192
193  def StoreToCacheDir(self, cache_dir):
194    # Create the dir if it doesn't exist.
195    command = "mkdir -p %s" % cache_dir
196    ret = self._ce.RunCommand(command)
197    if ret:
198      raise Exception("Could not create cache dir: %s" % cache_dir)
199    # Store to the cache directory.
200    with open(os.path.join(cache_dir, RESULTS_FILE), "w") as f:
201      pickle.dump(self.out, f)
202      pickle.dump(self.err, f)
203      pickle.dump(self.retval, f)
204
205    tarball = os.path.join(cache_dir, AUTOTEST_TARBALL)
206    command = ("cd %s && tar cjf %s ." % (self.results_dir, tarball))
207    ret = self._ce.RunCommand(command)
208    if ret:
209      raise Exception("Couldn't store autotest output directory.")
210
211  @classmethod
212  def CreateFromRun(cls, logger, chromeos_root, board, out, err, retval):
213    result = cls(logger)
214    result._PopulateFromRun(chromeos_root, board, out, err, retval)
215    return result
216
217  @classmethod
218  def CreateFromCacheHit(cls, logger, cache_dir):
219    result = cls(logger)
220    try:
221      result._PopulateFromCacheDir(cache_dir)
222    except Exception as e:
223      logger.LogError("Exception while using cache: %s" % e)
224      return None
225    return result
226
227
228class CacheConditions(object):
229  # Cache hit only if the result file exists.
230  CACHE_FILE_EXISTS = 0
231
232  # Cache hit if the ip address of the cached result and the new run match.
233  REMOTES_MATCH = 1
234
235  # Cache hit if the image checksum of the cached result and the new run match.
236  CHECKSUMS_MATCH = 2
237
238  # Cache hit only if the cached result was successful
239  RUN_SUCCEEDED = 3
240
241  # Never a cache hit.
242  FALSE = 4
243
244  # Cache hit if the image path matches the cached image path.
245  IMAGE_PATH_MATCH = 5
246
247
248class ResultsCache(object):
249  """ This class manages the key of the cached runs without worrying about what
250  is exactly stored (value). The value generation is handled by the Results
251  class.
252  """
253  CACHE_VERSION = 3
254  def Init(self, chromeos_image, chromeos_root, autotest_name, iteration,
255           autotest_args, remote, board, cache_conditions,
256           logger_to_use):
257    self.chromeos_image = chromeos_image
258    self.chromeos_root = chromeos_root
259    self.autotest_name = autotest_name
260    self.iteration = iteration
261    self.autotest_args = autotest_args,
262    self.remote = remote
263    self.board = board
264    self.cache_conditions = cache_conditions
265    self._logger = logger_to_use
266    self._ce = command_executer.GetCommandExecuter(self._logger)
267
268  def _GetCacheDirForRead(self):
269    glob_path = self._FormCacheDir(self._GetCacheKeyList(True))
270    matching_dirs = glob.glob(glob_path)
271
272    if matching_dirs:
273      # Cache file found.
274      if len(matching_dirs) > 1:
275        self._logger.LogError("Multiple compatible cache files: %s." %
276                              " ".join(matching_dirs))
277      return matching_dirs[0]
278    else:
279      return None
280
281  def _GetCacheDirForWrite(self):
282    return self._FormCacheDir(self._GetCacheKeyList(False))
283
284  def _FormCacheDir(self, list_of_strings):
285    cache_key = " ".join(list_of_strings)
286    cache_dir = misc.GetFilenameFromString(cache_key)
287    cache_path = os.path.join(SCRATCH_DIR, cache_dir)
288    return cache_path
289
290  def _GetCacheKeyList(self, read):
291    if read and CacheConditions.REMOTES_MATCH not in self.cache_conditions:
292      remote = "*"
293    else:
294      remote = self.remote
295    if read and CacheConditions.CHECKSUMS_MATCH not in self.cache_conditions:
296      checksum = "*"
297    else:
298      checksum = ImageChecksummer().Checksum(self.chromeos_image)
299
300    if read and CacheConditions.IMAGE_PATH_MATCH not in self.cache_conditions:
301      image_path_checksum = "*"
302    else:
303      image_path_checksum = hashlib.md5(self.chromeos_image).hexdigest()
304
305    autotest_args_checksum = hashlib.md5(
306                             "".join(self.autotest_args)).hexdigest()
307
308    return (image_path_checksum,
309            self.autotest_name, str(self.iteration),
310            autotest_args_checksum,
311            checksum,
312            remote,
313            str(self.CACHE_VERSION))
314
315  def ReadResult(self):
316    if CacheConditions.FALSE in self.cache_conditions:
317      return None
318    cache_dir = self._GetCacheDirForRead()
319
320    if not cache_dir:
321      return None
322
323    if not os.path.isdir(cache_dir):
324      return None
325
326    self._logger.LogOutput("Trying to read from cache dir: %s" % cache_dir)
327
328    result = Result.CreateFromCacheHit(self._logger, cache_dir)
329
330    if not result:
331      return None
332
333    if (result.retval == 0 or
334        CacheConditions.RUN_SUCCEEDED not in self.cache_conditions):
335      return result
336
337    return None
338
339  def StoreResult(self, result):
340    cache_dir = self._GetCacheDirForWrite()
341    result.StoreToCacheDir(cache_dir)
342
343
344class MockResultsCache(object):
345  def Init(self, *args):
346    pass
347
348  def ReadResult(self):
349    return Result("Results placed in /tmp/test", "", 0)
350
351  def StoreResult(self, result):
352    pass
353