[dvsim] Better in-progress print messages

- This PR updates the dvsim on-going progress to print valuable
information in terms of how many queued, dispatched, passed, failed, killed and
total. This helps keep things clear.

Signed-off-by: Srikrishna Iyer <sriyer@google.com>
diff --git a/util/dvsim/Deploy.py b/util/dvsim/Deploy.py
index 903660c..7f65dd0 100644
--- a/util/dvsim/Deploy.py
+++ b/util/dvsim/Deploy.py
@@ -25,10 +25,14 @@
     Abstraction for deploying builds and runs.
     """
 
+    # Timer.
+    num_secs = 0
+
     # Maintain a list of dispatched items.
     dispatch_counter = 0
 
     # Misc common deploy settings.
+    print_legend = True
     print_interval = 5
     max_parallel = 16
     max_odirs = 5
@@ -52,6 +56,7 @@
         self.sim_cfg = sim_cfg
 
         # Common vars
+        self.identifier = ""
         self.cmd = ""
         self.odir = ""
         self.log = ""
@@ -123,6 +128,9 @@
         self.__dict__ = find_and_substitute_wildcards(self.__dict__,
                                                       self.__dict__)
 
+        # Set identifier.
+        self.identifier = self.sim_cfg.name + ":" + self.name
+
         # Set the command, output dir and log
         self.odir = getattr(self, self.target + "_dir")
         # Set the output dir link name to the basename of odir (by default)
@@ -170,7 +178,7 @@
                                             stderr=f,
                                             env=self.exports)
             self.log_fd = f
-            self.status = "."
+            self.status = "D"
             Deploy.dispatch_counter += 1
         except IOError:
             log.error('IO Error: See %s', self.log)
@@ -279,7 +287,7 @@
                 log.error("Cmd \"%s\" could not be run", cmd)
 
     def get_status(self):
-        if self.status != ".": return
+        if self.status != "D": return
         if self.process.poll() is not None:
             self.log_fd.close()
             if self.process.returncode != 0:
@@ -295,8 +303,8 @@
             else:
                 self.set_status()
 
-            log.log(VERBOSE, "Item %s has completed execution: %s", self.name,
-                    self.status)
+            log.debug("Item %s has completed execution: %s", self.name,
+                      self.status)
             Deploy.dispatch_counter -= 1
             self.link_odir()
             del self.process
@@ -304,95 +312,116 @@
     @staticmethod
     def deploy(items):
         dispatched_items = []
+        queued_items = items
+
+        if Deploy.print_legend:
+            # Print legend once at the start of the run.
+            log.info("[legend]: [Q: queued, D: dispatched, " + \
+                     "P: passed, F: failed, K: killed, T: total]")
+            Deploy.print_legend = False
+
+        def get_etime():
+            # Convert num_secs to hh:mm:ss
+            hh = Deploy.num_secs // (3600)
+            ss = Deploy.num_secs % (3600)
+            mm = ss // 60
+            ss %= 60
+            return "%02i:%02i:%02i" % (hh, mm, ss)
 
         def dispatch_items(items):
             item_names = {}
             for item in items:
                 if item.target not in item_names.keys():
-                    item_names[item.target] = "["
+                    item_names[item.target] = ""
                 if item.status is None:
-                    item_names[item.target] += "  "
-                    if log.getLogger().isEnabledFor(VERBOSE):
-                        item_names[
-                            item.target] += item.name + ":" + item.log + ",\n"
-                    else:
-                        item_names[item.target] += item.odir_ln + ", "
+                    item_names[item.target] += item.identifier + ", "
                     item.dispatch_cmd()
-                    dispatched_items.append(item)
 
             for target in item_names.keys():
-                if item_names[target] != "[":
-                    item_names[target] = " [" + item_names[target][3:]
-                    item_names[target] = item_names[target][:-2] + "]"
-                    log.info("[dvsim]: %s:\n%s", target, item_names[target])
+                if item_names[target] != "":
+                    item_names[target] = "  [" + item_names[target][:-2] + "]"
+                    log.log(VERBOSE, "[%s]: [%s]: [dispatch]:\n%s",
+                            get_etime(), target, item_names[target])
 
-        # Dispatch the given items
-        dispatch_items_queue = []
-        if len(items) > Deploy.max_parallel:
-            dispatch_items(items[0:Deploy.max_parallel])
-            dispatch_items_queue = items[Deploy.max_parallel:]
-        else:
-            dispatch_items(items)
+        def track_progress(status, print_status):
+            all_done = True
+            for target in status.keys():
+                if "target_done" in status[target].keys(): continue
+                stats = {}
+                stats["Q"] = 0
+                stats["D"] = 0
+                stats["P"] = 0
+                stats["F"] = 0
+                stats["K"] = 0
+                stats["T"] = 0
+                target_done = False
+                for item in status[target].keys():
+                    stats[status[target][item]] += 1
+                    stats["T"] += 1
+                if stats["Q"] == 0 and stats["D"] == 0:
+                    target_done = True
+                all_done &= target_done
+
+                if print_status:
+                    width = "0{}d".format(len(str(stats["T"])))
+                    msg = "["
+                    for s in stats.keys():
+                        msg += s + ": {:{}}, ".format(stats[s], width)
+                    msg = msg[:-2] + "]"
+                    log.info("[%s]: [%s]: %s", get_etime(), target, msg)
+                    if target_done: status[target]["target_done"] = True
+            return all_done
 
         all_done = False
-        num_secs = 0
         status = {}
-        status_str = {}
-        status_str_prev = {}
+
+        # Queue all items
+        for item in queued_items:
+            if item.target not in status.keys():
+                status[item.target] = {}
+            status[item.target][item] = "Q"
 
         while not all_done:
-            time.sleep(1)
-            num_secs += 1
-            trig_print = ((num_secs % Deploy.print_interval) == 0)
-            for item in dispatched_items:
-                if item.target not in status.keys():
-                    status[item.target] = {}
-                if item not in status[item.target].keys():
-                    status[item.target][item] = ""
-
-                if item.status == ".": item.get_status()
-                if item.status != status[
-                        item.target][item] and item.status != ".":
-                    trig_print = True
-                    if item.status != "P":
-                        # Kill sub items
-                        item.set_sub_status("K")
-                    dispatch_items_queue.extend(item.sub)
-                status[item.target][item] = item.status
-
-            # Dispatch more from the queue
-            if len(dispatch_items_queue) == 0:
-                all_done = True
-            else:
+            # Dispatch items from the queue as slots free up.
+            if len(queued_items) != 0:
                 num_slots = Deploy.max_parallel - Deploy.dispatch_counter
                 if num_slots > 0:
-                    if len(dispatch_items_queue) > num_slots:
-                        dispatch_items(dispatch_items_queue[0:num_slots])
-                        dispatch_items_queue = dispatch_items_queue[num_slots:]
+                    if len(queued_items) > num_slots:
+                        dispatch_items(queued_items[0:num_slots])
+                        dispatched_items.extend(queued_items[0:num_slots])
+                        queued_items = queued_items[num_slots:]
                     else:
-                        dispatch_items(dispatch_items_queue)
-                        dispatch_items_queue = []
+                        dispatch_items(queued_items)
+                        dispatched_items.extend(queued_items)
+                        queued_items = []
 
-            status_str_prev = status_str.copy()
-            status_str = {}
-            for target in status.keys():
-                if target not in status_str.keys(): status_str[target] = "["
-                for item in status[target].keys():
-                    if status[target][item] is not None:
-                        status_str[target] += status[target][item]
-                        if status[target][item] == ".":
-                            all_done = False
-                status_str[target] += "]"
+            # Advance time by 1s.
+            time.sleep(1)
+            Deploy.num_secs += 1
+            print_status = ((Deploy.num_secs % Deploy.print_interval) == 0)
 
-            # Print the status string periodically
-            if trig_print:
-                for target in status_str.keys():
-                    if (target in status_str_prev.keys()) and \
-                       (status_str[target] == status_str_prev[target]) and \
-                       (status_str[target].find(".") == -1):
-                        continue
-                    log.info("[dvsim]: [%06ds] [%s]: %s", num_secs, target,
-                             status_str[target])
+            # Get status of dispatched items.
+            for item in dispatched_items:
+                if item.status == "D": item.get_status()
+                if item.status != status[item.target][item]:
+                    print_status = True
+                    if item.status != "D":
+                        if item.status != "P":
+                            # Kill its sub items if item did not pass.
+                            item.set_sub_status("K")
+                        log.log(VERBOSE, "[%s]: [%s]: [status]\n  [%s: %s]",
+                                get_etime(), item.target, item.identifier,
+                                item.status)
+                        # Queue items' sub-items if it is done.
+                        queued_items.extend(item.sub)
+                        for sub_item in item.sub:
+                            if sub_item.target not in status.keys():
+                                status[sub_item.target] = {}
+                            status[sub_item.target][sub_item] = "Q"
+                status[item.target][item] = item.status
+
+            # Check if we are done and print the status periodically.
+            all_done = track_progress(status, print_status)
 
 
 class CompileSim(Deploy):
@@ -525,8 +554,8 @@
             "run_opts": False,
             "sw_dir": False,
             "sw_name": False,
-            "sw_build_device":False,
-            "sw_build_dir":False,
+            "sw_build_device": False,
+            "sw_build_dir": False,
             "run_dir": False,
             "run_cmd": False,
             "run_opts": False
@@ -558,16 +587,21 @@
         # Start fail message construction
         self.fail_msg = "\n**TEST:** {}, ".format(self.name)
         self.fail_msg += "**SEED:** {}<br>\n".format(self.seed)
-        log_sub_path = self.log.replace(self.sim_cfg.scratch_root + '/', '')
-        self.fail_msg += "**LOG:** {}<br>\n".format(log_sub_path)
+        log_sub_path = self.log.replace(self.sim_cfg.scratch_path + '/', '')
+        self.fail_msg += "**LOG:** $scratch_path/{}<br>\n".format(log_sub_path)
 
         RunTest.items.append(self)
 
+    def __post_init__(self):
+        super().__post_init__()
+        # Set identifier.
+        self.identifier = self.sim_cfg.name + ":" + self.run_dir_name
+
     def get_status(self):
         '''Override base class get_status implementation for additional post-status
         actions.'''
         super().get_status()
-        if self.status not in [".", "P"]:
+        if self.status not in ["D", "P"]:
             # Delete the coverage data if available.
             if os.path.exists(self.cov_db_test_dir):
                 log.log(VERBOSE, "Deleting coverage data of failing test:\n%s",