Cloned from CL 57424 by 'g4 patch'.
Original change by raymes@raymes-crosstool-git5-11blah-git5 on 2011/12/14 11:43:10.

-Added the failure reason of jobs to the text report.
-Added some logging about cache hit/misses.
-Allowed checksums to be computed simultaneously
-Renamed action_runner to experiment_runner
-Moved storing results logic to experiment_runner
-Improvements to logging

Added the following fixes:
-Don't store run results when Ctrl-C is pressed in the middle of a run.
-Refactor caching code a bit to make it clearer.
-Fixed exception thrown when the cache was empty.
-Changed profile_type to be "" when it is invalid instead of "none".
-Fixed exception when using cache if no profile is present in the cache
file.
-Made ExecuteCommandInChroot() thread- and process-safe.

Added a DIFFBASE= for convenience.

PRESUBMIT=passed
R=raymes,bjanakiraman,shenhan
DELTA=412  (163 added, 183 deleted, 66 changed)
OCL=57454-p2
RCL=57583-p2
RDATE=2011/12/27 14:17:33
DIFFBASE=57424-p2


P4 change: 42662702
diff --git a/v14/crosperf/action_runner.py b/v14/crosperf/action_runner.py
deleted file mode 100644
index c77ebca..0000000
--- a/v14/crosperf/action_runner.py
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/usr/bin/python
-
-# Copyright 2011 Google Inc. All Rights Reserved.
-
-from experiment_status import ExperimentStatus
-from results_report import HTMLResultsReport
-from results_report import TextResultsReport
-from utils import logger
-from utils.email_sender import EmailSender
-
-
-class ActionRunner(object):
-  def __init__(self, experiment):
-    self._experiment = experiment
-    self.l = logger.GetLogger()
-
-  def Run(self, experiment):
-    status = ExperimentStatus(experiment)
-    experiment.start()
-    try:
-      while not experiment.complete:
-        border = "=============================="
-        self.l.LogOutput(border)
-        self.l.LogOutput(status.GetProgressString())
-        self.l.LogOutput(status.GetStatusString())
-        logger.GetLogger().LogOutput(border)
-        experiment.join(30)
-    except KeyboardInterrupt:
-      self.l.LogError("Ctrl-c pressed. Cleaning up...")
-      experiment.terminate = True
-
-  def PrintTable(self, experiment):
-    if experiment.complete:
-      self.l.LogOutput(TextResultsReport(experiment).GetReport())
-
-  def Email(self, experiment):
-    # Only email by default if a new run was completed.
-    send_mail = False
-    for benchmark_run in experiment.benchmark_runs:
-      if not benchmark_run.cache_hit:
-        send_mail = True
-        break
-    if not send_mail:
-      return
-
-    if experiment.complete:
-      label_names = []
-      for label in experiment.labels:
-        label_names.append(label.name)
-      subject = "%s: %s" % (experiment.name, " vs. ".join(label_names))
-
-      text_report = TextResultsReport(experiment).GetReport()
-      text_report = "<pre style='font-size: 13px'>%s</pre>" % text_report
-      html_report = HTMLResultsReport(experiment).GetReport()
-      attachment = EmailSender.Attachment("report.html", html_report)
-      EmailSender().SendEmailToUser(subject,
-                                    text_report,
-                                    attachments=[attachment],
-                                    msg_type="html")
-
-  def StoreResults (self, experiment):
-    experiment.StoreResults()
-
-  def RunActions(self):
-    self.Run(self._experiment)
-    self.PrintTable(self._experiment)
-    self.Email(self._experiment)
-    self.StoreResults(self._experiment)
-
-
-class MockActionRunner(ActionRunner):
-  def __init__(self, experiment):
-    super(MockActionRunner, self).__init__(experiment)
-
-  def Run(self, experiment):
-    self.l.LogOutput("Would run the following experiment: '%s'." %
-                     experiment.name)
-
-  def PrintTable(self, experiment):
-    self.l.LogOutput("Would print the experiment table.")
-
-  def Email(self, experiment):
-    self.l.LogOutput("Would send result email.")
-
-  def StoreResults(self, experiment):
-    self.l.LogOutput("Would store the results.")
diff --git a/v14/crosperf/autotest_runner.py b/v14/crosperf/autotest_runner.py
index 2e9b25d..8958663 100644
--- a/v14/crosperf/autotest_runner.py
+++ b/v14/crosperf/autotest_runner.py
@@ -9,10 +9,11 @@
 class AutotestRunner(object):
   def __init__(self):
     self._ce = command_executer.GetCommandExecuter()
+    self._ct = command_executer.CommandTerminator()
 
   def Run(self, machine_name, chromeos_root, board, autotest_name,
           autotest_args, profile_counters, profile_type):
-    if profile_counters and profile_type != "none":
+    if profile_counters and profile_type:
       profiler_args = "-e " + " -e ".join(profile_counters)
       if profile_type == "record":
         profiler_args += "-g"
@@ -25,7 +26,10 @@
       options += " %s" % autotest_args
     command = ("./run_remote_tests.sh --remote=%s %s %s" %
                (machine_name, options, autotest_name))
-    return utils.ExecuteCommandInChroot(chromeos_root, command, True)
+    return utils.ExecuteCommandInChroot(chromeos_root, command, True, self._ct)
+
+  def Terminate(self):
+    self._ct.Terminate()
 
 
 class MockAutotestRunner(object):
diff --git a/v14/crosperf/benchmark_run.py b/v14/crosperf/benchmark_run.py
index 237944c..3d20d2e 100644
--- a/v14/crosperf/benchmark_run.py
+++ b/v14/crosperf/benchmark_run.py
@@ -10,7 +10,6 @@
 import traceback
 from results_cache import Result
 from utils import logger
-from utils.file_utils import FileUtils
 
 STATUS_FAILED = "FAILED"
 STATUS_SUCCEEDED = "SUCCEEDED"
@@ -39,7 +38,7 @@
     self.board = board
     self.iteration = iteration
     self.results = {}
-    self.terminate = False
+    self.terminated = False
     self.retval = None
     self.status = STATUS_PENDING
     self.run_completed = False
@@ -56,6 +55,7 @@
     self.runs_complete = 0
     self.cache_hit = False
     self.perf_results = None
+    self.failure_reason = ""
 
   def MeanExcludingOutliers(self, array, outlier_range):
     """Return the arithmetic mean excluding outliers."""
@@ -97,17 +97,19 @@
 
     # Store the autotest output in the cache also.
     if not cache_hit:
+      self.cache.StoreResult(result)
       self.cache.StoreAutotestOutput(results_dir)
 
     # Generate a perf report and cache it.
-    if cache_hit:
-      self.perf_results = self.cache.ReadPerfResults()
-    else:
-      self.perf_results = (self.perf_processor.
-                           GeneratePerfResults(results_dir,
-                                               self.chromeos_root,
-                                               self.board))
-      self.cache.StorePerfResults(self.perf_results)
+    if self.profile_type:
+      if cache_hit:
+        self.perf_results = self.cache.ReadPerfResults()
+      else:
+        self.perf_results = (self.perf_processor.
+                             GeneratePerfResults(results_dir,
+                                                 self.chromeos_root,
+                                                 self.board))
+        self.cache.StorePerfResults(self.perf_results)
 
     # If there are valid results from perf stat, combine them with the
     # autotest results.
@@ -115,21 +117,6 @@
       stat_results = self.perf_processor.ParseStatResults(self.perf_results)
       self.results = dict(self.results.items() + stat_results.items())
 
-  def StoreResults(self, results_dir):
-    # Store perf_report and autotest output locally.
-    try:
-      self.cache.ReadAutotestOutput(results_dir)
-    except Exception, e:
-      self._logger.LogError(e)
-    try:
-      if self.perf_results:
-        FileUtils().WriteFile(os.path.join(results_dir, "perf.report"),
-                              self.perf_results.report)
-        FileUtils().WriteFile(os.path.join(results_dir, "perf.out"),
-                              self.perf_results.output)
-    except Exception, e:
-      self._logger.LogError(e)
-
   def _GetResultsDir(self, output):
     mo = re.search("Results placed in (\S+)", output)
     if mo:
@@ -155,34 +142,50 @@
       self.cache_hit = (result is not None)
 
       if result:
+        self._logger.LogOutput("%s: Cache hit." % self.name)
         self._logger.LogOutput(result.out + "\n" + result.err)
       else:
+        self._logger.LogOutput("%s: No cache hit." % self.name)
         self.status = STATUS_WAITING
         # Try to acquire a machine now.
         self.machine = self.AcquireMachine()
         self.cache.remote = self.machine.name
         result = self.RunTest(self.machine)
 
+      if self.terminated:
+        return
+
       if not result.retval:
         self.status = STATUS_SUCCEEDED
       else:
-        self.status = STATUS_FAILED
+        if self.status != STATUS_FAILED:
+          self.status = STATUS_FAILED
+          self.failure_reason = "Return value of autotest was non-zero."
 
       self.ProcessResults(result, self.cache_hit)
 
     except Exception, e:
-      self._logger.LogError("Benchmark run: '%s' failed: %s." % (self.name, e))
+      self._logger.LogError("Benchmark run: '%s' failed: %s" % (self.name, e))
       traceback.print_exc()
-      self.status = STATUS_FAILED
+      if self.status != STATUS_FAILED:
+        self.status = STATUS_FAILED
+        self.failure_reason = str(e)
     finally:
       if self.machine:
         self._logger.LogOutput("Releasing machine: %s" % self.machine.name)
         self.machine_manager.ReleaseMachine(self.machine)
         self._logger.LogOutput("Released machine: %s" % self.machine.name)
 
+  def Terminate(self):
+    self.terminated = True
+    self.autotest_runner.Terminate()
+    if self.status != STATUS_FAILED:
+      self.status = STATUS_FAILED
+      self.failure_reason = "Thread terminated."
+
   def AcquireMachine(self):
     while True:
-      if self.terminate:
+      if self.terminated:
         raise Exception("Thread terminated while trying to acquire machine.")
       machine = self.machine_manager.AcquireMachine(self.chromeos_image)
       if machine:
@@ -212,7 +215,6 @@
     self.run_completed = True
     result = Result(out, err, retval)
 
-    self.cache.StoreResult(result)
     return result
 
   def SetCacheConditions(self, cache_conditions):
diff --git a/v14/crosperf/column_chart.py b/v14/crosperf/column_chart.py
index f86ab1e..22a45c5 100644
--- a/v14/crosperf/column_chart.py
+++ b/v14/crosperf/column_chart.py
@@ -54,4 +54,4 @@
     return res
 
   def GetDiv(self):
-    return "<div id='%s'></div>" % self.chart_div
+    return "<div id='%s' class='chart'></div>" % self.chart_div
diff --git a/v14/crosperf/crosperf.py b/v14/crosperf/crosperf.py
index 384d960..e00ae75 100755
--- a/v14/crosperf/crosperf.py
+++ b/v14/crosperf/crosperf.py
@@ -8,8 +8,8 @@
 import optparse
 import os
 import sys
-from action_runner import ActionRunner
-from action_runner import MockActionRunner
+from experiment_runner import ExperimentRunner
+from experiment_runner import MockExperimentRunner
 from experiment_factory import ExperimentFactory
 from experiment_file import ExperimentFile
 from help import Help
@@ -85,10 +85,10 @@
   atexit.register(Cleanup, experiment)
 
   if options.dry_run:
-    runner = MockActionRunner(experiment)
+    runner = MockExperimentRunner(experiment)
   else:
-    runner = ActionRunner(experiment)
-  runner.RunActions()
+    runner = ExperimentRunner(experiment)
+  runner.Run()
 
 if __name__ == "__main__":
   Main(sys.argv)
diff --git a/v14/crosperf/experiment.py b/v14/crosperf/experiment.py
index 6c24dcc..882af71 100644
--- a/v14/crosperf/experiment.py
+++ b/v14/crosperf/experiment.py
@@ -3,7 +3,6 @@
 # Copyright 2011 Google Inc. All Rights Reserved.
 
 import os
-import threading
 import time
 from autotest_runner import AutotestRunner
 from benchmark_run import BenchmarkRun
@@ -15,21 +14,18 @@
 from utils.file_utils import FileUtils
 
 
-class Experiment(threading.Thread):
+class Experiment(object):
   """Class representing an Experiment to be run."""
 
   def __init__(self, name, remote, rerun_if_failed, working_directory,
                chromeos_root, cache_conditions, labels, benchmarks,
                experiment_file):
-    threading.Thread.__init__(self)
     self.name = name
     self.rerun_if_failed = rerun_if_failed
     self.working_directory = working_directory
     self.remote = remote
     self.chromeos_root = chromeos_root
     self.cache_conditions = cache_conditions
-    self.complete = False
-    self.terminate = False
     self.experiment_file = experiment_file
     self.results_directory = os.path.join(self.working_directory,
                                           self.name + "_results")
@@ -94,60 +90,27 @@
     for t in self.benchmark_runs:
       if t.isAlive():
         self.l.LogError("Terminating run: '%s'." % t.name)
-        t.terminate = True
+        t.Terminate()
 
-  def RunAutotestRunsInParallel(self):
-    active_threads = []
+  def IsComplete(self):
+    if self.active_threads:
+      for t in self.active_threads:
+        if t.isAlive():
+          t.join(0)
+        if not t.isAlive():
+          self.num_complete += 1
+          self.active_threads.remove(t)
+      return False
+    return True
+
+  def Run(self):
+    self.start_time = time.time()
+    self.active_threads = []
     for benchmark_run in self.benchmark_runs:
       # Set threads to daemon so program exits when ctrl-c is pressed.
       benchmark_run.daemon = True
       benchmark_run.start()
-      active_threads.append(benchmark_run)
-
-    try:
-      while active_threads:
-        if self.terminate:
-          self.Terminate()
-          return
-
-        for t in active_threads:
-          if t.isAlive():
-            t.join(1)
-          if not t.isAlive():
-            self.num_complete += 1
-            active_threads.remove(t)
-    except KeyboardInterrupt:
-      self.Terminate()
-      return
-    finally:
-      self.complete = True
-
-    self.l.LogOutput("Benchmark runs complete. Final status:")
-    for benchmark_run in self.benchmark_runs:
-      self.l.LogOutput("'%s'\t\t%s" % (benchmark_run.name,
-                                       benchmark_run.status))
-
-  def run(self):
-    self.start_time = time.time()
-    self.RunAutotestRunsInParallel()
-
-  def StoreResults(self):
-    FileUtils().RmDir(self.results_directory)
-    FileUtils().MkDirP(self.results_directory)
-    experiment_file_path = os.path.join(self.results_directory,
-                                        "experiment.exp")
-    FileUtils().WriteFile(experiment_file_path, self.experiment_file)
-
-    results_table_path = os.path.join(self.results_directory, "results.html")
-    report = HTMLResultsReport(self).GetReport()
-    FileUtils().WriteFile(results_table_path, report)
-
-    for benchmark_run in self.benchmark_runs:
-      benchmark_run_name = filter(str.isalnum, benchmark_run.name)
-      benchmark_run_path = os.path.join(self.results_directory,
-                                        benchmark_run_name)
-      FileUtils().MkDirP(benchmark_run_path)
-      benchmark_run.StoreResults(benchmark_run_path)
+      self.active_threads.append(benchmark_run)
 
   def SetCacheConditions(self, cache_conditions):
     for benchmark_run in self.benchmark_runs:
diff --git a/v14/crosperf/field.py b/v14/crosperf/field.py
index e433324..b3cdaa2 100644
--- a/v14/crosperf/field.py
+++ b/v14/crosperf/field.py
@@ -53,7 +53,12 @@
                                        description)
 
   def _Parse(self, value):
-    return bool(value)
+    if value.lower() == "true":
+      return True
+    elif value.lower() == "false":
+      return False
+    raise Exception("Invalid value for '%s'. Must be true or false." %
+                    self.name)
 
 
 class IntegerField(Field):
diff --git a/v14/crosperf/image_checksummer.py b/v14/crosperf/image_checksummer.py
index 38a4d7e..f75dc94 100644
--- a/v14/crosperf/image_checksummer.py
+++ b/v14/crosperf/image_checksummer.py
@@ -8,9 +8,24 @@
 
 
 class ImageChecksummer(object):
+  class PerImageChecksummer(object):
+    def __init__(self, filename):
+      self._lock = threading.Lock()
+      self.filename = filename
+      self._checksum = None
+
+    def Checksum(self):
+      with self._lock:
+        if not self._checksum:
+          logger.GetLogger().LogOutput("Computing checksum for '%s'." %
+                                       self.filename)
+          self._checksum = FileUtils().Md5File(self.filename)
+          logger.GetLogger().LogOutput("Checksum is: %s" % self._checksum)
+        return self._checksum
+
   _instance = None
   _lock = threading.Lock()
-  _checksums = {}
+  _per_image_checksummers = {}
 
   def __new__(cls, *args, **kwargs):
     with cls._lock:
@@ -21,15 +36,14 @@
 
   def Checksum(self, filename):
     with self._lock:
-      if filename in self._checksums:
-        return self._checksums[filename]
-      try:
-        logger.GetLogger().LogOutput("Computing checksum for '%s'." % filename)
-        checksum = FileUtils().Md5File(filename)
-        self._checksums[filename] = checksum
-        return checksum
+      if filename not in self._per_image_checksummers:
+        self._per_image_checksummers[filename] = (ImageChecksummer.
+                                                  PerImageChecksummer(filename))
+      checksummer = self._per_image_checksummers[filename]
 
-      except Exception, e:
-        logger.GetLogger().LogError("Could not compute checksum of file '%s'."
-                                    % filename)
-        raise e
+    try:
+      return checksummer.Checksum()
+    except Exception, e:
+      logger.GetLogger().LogError("Could not compute checksum of file '%s'."
+                                  % filename)
+      raise e
diff --git a/v14/crosperf/machine_manager.py b/v14/crosperf/machine_manager.py
index a267a0b..5beaf3b 100644
--- a/v14/crosperf/machine_manager.py
+++ b/v14/crosperf/machine_manager.py
@@ -102,6 +102,9 @@
         for m in self._all_machines:
           self._TryToLockMachine(m)
         self.initialized = True
+        for m in self._all_machines:
+          m.released_time = time.time()
+
       if not self._machines:
         machine_names = []
         for machine in self._all_machines:
@@ -123,6 +126,12 @@
           m.locked = True
           m.autotest_run = threading.current_thread()
           return m
+      # This logic ensures that threads waiting on a machine will get a machine
+      # with a checksum equal to their image over other threads. This saves time
+      # when crosperf initially assigns the machines to threads by minimizing
+      # the number of re-images.
+      # TODO(asharif): If we centralize the thread-scheduler, we wont need this
+      # code and can implement minimal reimaging code more cleanly.
       for m in [machine for machine in self._machines if not machine.locked]:
         if time.time() - m.released_time > 20:
           m.locked = True
@@ -208,4 +217,3 @@
 
   def GetMachines(self):
     return self.machines
-
diff --git a/v14/crosperf/results_cache.py b/v14/crosperf/results_cache.py
index 8868ae2..f4a41c6 100644
--- a/v14/crosperf/results_cache.py
+++ b/v14/crosperf/results_cache.py
@@ -62,7 +62,29 @@
       self._logger = logger.GetLogger()
     self._ce = command_executer.GetCommandExecuter(self._logger)
 
-  def _GetCacheDir(self, read=False):
+  def _GetCacheDirForRead(self):
+    glob_path = self._FormCacheDir(self._GetCacheKeyList(True))
+    matching_dirs = glob.glob(glob_path)
+
+    if matching_dirs:
+      # Cache file found.
+      if len(matching_dirs) > 1:
+        self._logger.LogError("Multiple compatible cache files: %s." %
+                              " ".join(matching_dirs))
+      return matching_dirs[0]
+    else:
+      return None
+
+  def _GetCacheDirForWrite(self):
+    return self._FormCacheDir(self._GetCacheKeyList(False))
+
+  def _FormCacheDir(self, list_of_strings):
+    cache_key = " ".join(list_of_strings)
+    cache_dir = self._ConvertToFilename(cache_key)
+    cache_path = os.path.join(SCRATCH_DIR, cache_dir)
+    return cache_path
+
+  def _GetCacheKeyList(self, read):
     if read and CacheConditions.REMOTES_MATCH not in self.cache_conditions:
       remote = "*"
     else:
@@ -71,27 +93,21 @@
       checksum = "*"
     else:
       checksum = ImageChecksummer().Checksum(self.chromeos_image)
-    ret = ("%s %s %s %s %s %s" %
-           (hashlib.md5(self.chromeos_image).hexdigest(),
-            self.autotest_name, self.iteration, ",".join(self.autotest_args),
-            checksum, remote))
-
-    return os.path.join(SCRATCH_DIR, self._ConvertToFilename(ret))
+    return (hashlib.md5(self.chromeos_image).hexdigest(),
+            self.autotest_name, str(self.iteration),
+            ",".join(self.autotest_args),
+            checksum, remote)
 
   def ReadResult(self):
     if CacheConditions.FALSE in self.cache_conditions:
-      self._logger.LogOutput("Cache condition FALSE passed. Not using cache.")
       return None
-    cache_dir = self._GetCacheDir(True)
-    matching_dirs = glob.glob(cache_dir)
+    cache_dir = self._GetCacheDirForRead()
 
-    if matching_dirs:
-      # Cache file found.
-      if len(matching_dirs) > 1:
-        self._logger.LogError("Multiple compatible cache files: %s." %
-                              " ".join(matching_dirs))
-      matching_dir = matching_dirs[0]
-      cache_file = os.path.join(matching_dir, RESULTS_FILE)
+    if not cache_dir:
+      return None
+
+    try:
+      cache_file = os.path.join(cache_dir, RESULTS_FILE)
 
       self._logger.LogOutput("Trying to read from cache file: %s" % cache_file)
 
@@ -104,16 +120,16 @@
             CacheConditions.RUN_SUCCEEDED not in self.cache_conditions):
           return Result(out, err, retval)
 
-    else:
+    except Exception, e:
       if CacheConditions.CACHE_FILE_EXISTS not in self.cache_conditions:
         # Cache file not found but just return a failure.
         return Result("", "", 1)
-      return None
+      raise e
 
   def StoreResult(self, result):
-    cache_dir = self._GetCacheDir()
+    cache_dir = self._GetCacheDirForWrite()
     cache_file = os.path.join(cache_dir, RESULTS_FILE)
-    command = "mkdir -p %s" % os.path.dirname(cache_file)
+    command = "mkdir -p %s" % cache_dir
     ret = self._ce.RunCommand(command)
     assert ret == 0, "Couldn't create cache dir"
     with open(cache_file, "wb") as f:
@@ -124,27 +140,32 @@
   def StoreAutotestOutput(self, results_dir):
     host_results_dir = os.path.join(self.chromeos_root, "chroot",
                                     results_dir[1:])
-    tarball = os.path.join(self._GetCacheDir(), AUTOTEST_TARBALL)
+    tarball = os.path.join(self._GetCacheDirForWrite(), AUTOTEST_TARBALL)
     command = ("cd %s && tar cjf %s ." % (host_results_dir, tarball))
     ret = self._ce.RunCommand(command)
     if ret:
       raise Exception("Couldn't store autotest output directory.")
 
   def ReadAutotestOutput(self, destination):
-    tarball = os.path.join(self._GetCacheDir(True), AUTOTEST_TARBALL)
+    cache_dir = self._GetCacheDirForWrite()
+    tarball = os.path.join(cache_dir, AUTOTEST_TARBALL)
+    if not os.path.exists(tarball):
+      raise Exception("Cached autotest tarball does not exist at '%s'." %
+                      tarball)
     command = ("cd %s && tar xjf %s ." % (destination, tarball))
     ret = self._ce.RunCommand(command)
     if ret:
       raise Exception("Couldn't read autotest output directory.")
 
   def StorePerfResults(self, perf):
-    perf_path = os.path.join(self._GetCacheDir(), PERF_RESULTS_FILE)
+    perf_path = os.path.join(self._GetCacheDirForWrite(), PERF_RESULTS_FILE)
     with open(perf_path, "wb") as f:
       pickle.dump(perf.report, f)
       pickle.dump(perf.output, f)
 
   def ReadPerfResults(self):
-    perf_path = os.path.join(self._GetCacheDir(), PERF_RESULTS_FILE)
+    cache_dir = self._GetCacheDirForRead()
+    perf_path = os.path.join(cache_dir, PERF_RESULTS_FILE)
     with open(perf_path, "rb") as f:
       report = pickle.load(f)
       output = pickle.load(f)
diff --git a/v14/crosperf/results_columns.py b/v14/crosperf/results_columns.py
index a9190b2..06f9c99 100644
--- a/v14/crosperf/results_columns.py
+++ b/v14/crosperf/results_columns.py
@@ -13,7 +13,7 @@
     for result in results:
       if isinstance(result, str):
         return True
-      return False
+    return False
 
   def _StripNone(self, results):
     res = []
@@ -27,20 +27,26 @@
   def Compute(self, results, baseline_results):
     if self._ContainsString(results):
       return "-"
-    return min(self._StripNone(results))
+    results = self._StripNone(results)
+    if not results:
+      return "-"
+    return min(results)
 
 
 class MaxColumn(Column):
   def Compute(self, results, baseline_results):
     if self._ContainsString(results):
       return "-"
-    return max(self._StripNone(results))
+    results = self._StripNone(results)
+    if not results:
+      return "-"
+    return max(results)
 
 
 class MeanColumn(Column):
   def Compute(self, results, baseline_results):
     all_pass = True
-    all_fail = False
+    all_fail = True
     if self._ContainsString(results):
       for result in results:
         if result != "PASSED":
@@ -53,9 +59,11 @@
       elif all_fail:
         return "ALL FAIL"
       else:
-        return "SOME FAIL"
+        return "-"
 
     results = self._StripNone(results)
+    if not results:
+      return "-"
     return float(sum(results)) / len(results)
 
 
@@ -68,6 +76,8 @@
       return "-"
 
     results = self._StripNone(results)
+    if not results:
+      return "-"
     n = len(results)
     average = sum(results) / n
     total = 0
@@ -87,6 +97,8 @@
 
     results = self._StripNone(results)
     baseline_results = self._StripNone(baseline_results)
+    if not results or not baseline_results:
+      return "-"
     result_mean = sum(results) / len(results)
     baseline_mean = sum(baseline_results) / len(baseline_results)
 
@@ -103,6 +115,8 @@
 
     results = self._StripNone(results)
     baseline_results = self._StripNone(baseline_results)
+    if not results or not baseline_results:
+      return "-"
     result_mean = sum(results) / len(results)
     baseline_mean = sum(baseline_results) / len(baseline_results)
 
diff --git a/v14/crosperf/results_report.py b/v14/crosperf/results_report.py
index bd369e8..ec6d7df 100644
--- a/v14/crosperf/results_report.py
+++ b/v14/crosperf/results_report.py
@@ -118,6 +118,13 @@
 ===========================================
 
 -------------------------------------------
+Benchmark Run Status
+-------------------------------------------
+%s
+
+Number re-images: %s
+
+-------------------------------------------
 Summary
 -------------------------------------------
 %s
@@ -137,8 +144,18 @@
   def __init__(self, experiment):
     super(TextResultsReport, self).__init__(experiment)
 
+  def GetStatusTable(self):
+    status_table = Table("status")
+    for benchmark_run in self.benchmark_runs:
+      status_table.AddRow([Table.Cell(benchmark_run.name),
+                           Table.Cell(benchmark_run.status),
+                           Table.Cell(benchmark_run.failure_reason)])
+    return status_table
+
   def GetReport(self):
     return self.TEXT % (self.experiment.name,
+                        self.GetStatusTable().ToText(),
+                        self.experiment.machine_manager.num_reimages,
                         self.GetSummaryTable().ToText(30),
                         self.GetFullTable().ToText(30),
                         self.experiment.experiment_file)
@@ -161,6 +178,10 @@
   font-size: 14px;
 }
 
+.chart {
+  display: inline;
+}
+
 .hidden {
   visibility: hidden;
 }
diff --git a/v14/crosperf/settings_factory.py b/v14/crosperf/settings_factory.py
index 38b5dd5..3f75283 100644
--- a/v14/crosperf/settings_factory.py
+++ b/v14/crosperf/settings_factory.py
@@ -31,9 +31,9 @@
                             "collect."))
     self.AddField(EnumField("profile_type",
                             description="The type of profile to collect. "
-                            "Either 'stat', 'record' or 'none'.",
-                            options=["stat", "record", "none"],
-                            default="none"))
+                            "Either 'stat', 'record' or ''.",
+                            options=["stat", "record", ""],
+                            default=""))
 
 
 class LabelSettings(Settings):
@@ -84,8 +84,8 @@
                             "collect."))
     self.AddField(EnumField("profile_type",
                             description="The type of profile to collect. "
-                            "Either 'stat', 'record' or 'none'.",
-                            options=["stat", "record", "none"]))
+                            "Either 'stat', 'record' or ''.",
+                            options=["stat", "record", ""]))
 
 
 class SettingsFactory(object):
diff --git a/v14/utils/command_executer.py b/v14/utils/command_executer.py
index 3373548..37d6e81 100644
--- a/v14/utils/command_executer.py
+++ b/v14/utils/command_executer.py
@@ -44,7 +44,10 @@
     self.logger.LogCmd(cmd, machine, username)
     if command_terminator and command_terminator.IsTerminated():
       self.logger.LogError("Command was terminated!")
-      return 1
+      if return_output:
+        return [1, "", ""]
+      else:
+        return 1
 
     if machine is not None:
       user = ""
@@ -52,7 +55,6 @@
         user = username + "@"
       cmd = "ssh -t -t %s%s -- '%s'" % (user, machine, cmd)
 
-
     pty_fds = pty.openpty()
     p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE,
@@ -74,7 +76,10 @@
         self.RunCommand("sudo kill -9 " + str(p.pid))
         wait = p.wait()
         self.logger.LogError("Command was terminated!")
-        return wait
+        if return_output:
+          return (p.wait, full_stdout, full_stderr)
+        else:
+          return wait
       for fd in fds[0]:
         if fd == p.stdout:
           out = os.read(p.stdout.fileno(), 16384)
diff --git a/v14/utils/utils.py b/v14/utils/utils.py
index a6f1553..d82b243 100755
--- a/v14/utils/utils.py
+++ b/v14/utils/utils.py
@@ -12,6 +12,7 @@
 import stat
 import command_executer
 import logger
+import tempfile
 from contextlib import contextmanager
 
 
@@ -67,17 +68,26 @@
   return "./setup_board --board=%s %s" % (board, " ".join(options))
 
 
-def ExecuteCommandInChroot(chromeos_root, command, return_output=False):
+def ExecuteCommandInChroot(chromeos_root, command, return_output=False,
+                           command_terminator=None):
   ce = command_executer.GetCommandExecuter()
-  command_file = "in_chroot_cmd.sh"
-  command_file_path = os.path.join(chromeos_root, "src/scripts", command_file)
-  with open(command_file_path, "w") as f:
+  handle, command_file = tempfile.mkstemp(dir=os.path.join(chromeos_root,
+                                                           "src/scripts"),
+                                          suffix=".sh",
+                                          prefix="in_chroot_cmd")
+  # Without this, the handle remains open and we get "file busy" when executing
+  # cros_sdk -- ./<file>.
+  os.close(handle)
+  with open(command_file, "w") as f:
     print >> f, "#!/bin/bash"
     print >> f, command
-  os.chmod(command_file_path, 0777)
+  os.chmod(command_file, 0777)
   with WorkingDirectory(chromeos_root):
-    command = "cros_sdk -- ./%s" % command_file
-    return ce.RunCommand(command, return_output)
+    command = "cros_sdk -- ./%s" % os.path.basename(command_file)
+    ret = ce.RunCommand(command, return_output,
+                        command_terminator=command_terminator)
+    os.remove(command_file)
+    return ret
 
 
 def CanonicalizePath(path):
@@ -111,4 +121,3 @@
     msg = "cd %s" % old_dir
     logger.GetLogger().LogCmd(msg)
   os.chdir(old_dir)
-