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