Repository: Qiskit/qiskit

Issue:
PassManager should record execution time for each pass 
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
The `PassManager` should record the execution time for each pass that it runs, and this information should be easily retrievable.




Discussion:
I agree..

Resolving patch:
diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py
--- a/qiskit/transpiler/passmanager.py
+++ b/qiskit/transpiler/passmanager.py
@@ -15,6 +15,8 @@
 
 from functools import partial
 from collections import OrderedDict
+from time import time
+
 from qiskit.dagcircuit import DAGCircuit
 from qiskit.converters import circuit_to_dag, dag_to_circuit
 from qiskit.visualization import pass_manager_drawer
@@ -61,6 +63,10 @@ def __init__(self, passes=None,
         self.passmanager_options = {'ignore_requires': ignore_requires,
                                     'ignore_preserves': ignore_preserves,
                                     'max_iteration': max_iteration}
+
+        # The property log_passes allows to log and time the passes as they run in the pass manager
+        self.log_passes = False
+
         if passes is not None:
             self.append(passes)
 
@@ -175,25 +181,31 @@ def _do_pass(self, pass_, dag, options):
 
         # Run the pass itself, if not already run
         if pass_ not in self.valid_passes:
-            if pass_.is_transformation_pass:
-                pass_.property_set = self.fenced_property_set
-                new_dag = pass_.run(dag)
-                if not isinstance(new_dag, DAGCircuit):
-                    raise TranspilerError("Transformation passes should return a transformed dag."
-                                          "The pass %s is returning a %s" % (type(pass_).__name__,
-                                                                             type(new_dag)))
-                dag = new_dag
-            elif pass_.is_analysis_pass:
-                pass_.property_set = self.property_set
-                pass_.run(FencedDAGCircuit(dag))
-            else:
-                raise TranspilerError("I dont know how to handle this type of pass")
+            dag = self._run_this_pass(pass_, dag)
 
             # update the valid_passes property
             self._update_valid_passes(pass_, options['ignore_preserves'])
 
         return dag
 
+    def _run_this_pass(self, pass_, dag):
+        if pass_.is_transformation_pass:
+            pass_.property_set = self.fenced_property_set
+            with PassManagerContext(self, pass_):
+                new_dag = pass_.run(dag)
+            if not isinstance(new_dag, DAGCircuit):
+                raise TranspilerError("Transformation passes should return a transformed dag."
+                                      "The pass %s is returning a %s" % (type(pass_).__name__,
+                                                                         type(new_dag)))
+            dag = new_dag
+        elif pass_.is_analysis_pass:
+            pass_.property_set = self.property_set
+            with PassManagerContext(self, pass_):
+                pass_.run(FencedDAGCircuit(dag))
+        else:
+            raise TranspilerError("I dont know how to handle this type of pass")
+        return dag
+
     def _update_valid_passes(self, pass_, ignore_preserves):
         self.valid_passes.add(pass_)
         if not pass_.is_analysis_pass:  # Analysis passes preserve all
@@ -214,6 +226,37 @@ def passes(self):
         return ret
 
 
+class PassManagerContext:
+    """ A wrap around the execution of a pass."""
+
+    def __init__(self, pm_instance, pass_instance):
+        self.pm_instance = pm_instance
+        self.pass_instance = pass_instance
+        self.start_time = None
+
+    def __enter__(self):
+        if self.pm_instance.log_passes:
+            self.start_time = time()
+
+    def __exit__(self, *exc_info):
+        if self.pm_instance.log_passes:
+            end_time = time()
+            raw_log_dict = {
+                'name': self.pass_instance.name(),
+                'start_time': self.start_time,
+                'end_time': end_time,
+                'running_time': end_time - self.start_time
+            }
+            log_dict = "%s: %.5f (ms)" % (self.pass_instance.name(),
+                                          (end_time - self.start_time) * 1000)
+            if self.pm_instance.property_set['pass_raw_log'] is None:
+                self.pm_instance.property_set['pass_raw_log'] = []
+            if self.pm_instance.property_set['pass_log'] is None:
+                self.pm_instance.property_set['pass_log'] = []
+            self.pm_instance.property_set['pass_raw_log'].append(raw_log_dict)
+            self.pm_instance.property_set['pass_log'].append(log_dict)
+
+
 class FlowController():
     """This class is a base class for multiple types of working list. When you iterate on it, it
     returns the next pass to run. """


Test patch:
diff --git a/test/python/transpiler/test_pass_scheduler.py b/test/python/transpiler/test_pass_scheduler.py
--- a/test/python/transpiler/test_pass_scheduler.py
+++ b/test/python/transpiler/test_pass_scheduler.py
@@ -600,8 +600,89 @@ def test_conditional_and_loop(self):
         self.assertEqual(expected, passmanager.passes())
 
 
+class TestLogPasses(QiskitTestCase):
+    """Testing the log_passes option."""
+
+    def setUp(self):
+        self.circuit = QuantumCircuit(QuantumRegister(1))
+
+    def assertPassLog(self, passmanager, list_of_passes):
+        """ Runs the passmanager and checks that the elements in
+        passmanager.property_set['pass_log'] match list_of_passes (the names)."""
+        transpile(self.circuit, pass_manager=passmanager)
+
+        current_names = [pass_log['name'] for pass_log in passmanager.property_set['pass_raw_log']]
+        self.assertEqual(current_names, list_of_passes)
+
+        # All the passes run under the second
+        current_running_time = [pass_log['running_time'] < 1 for pass_log in
+                                passmanager.property_set['pass_raw_log']]
+        self.assertTrue(all(current_running_time))
+
+        # The formatted property_set['pass_log']
+        formatted_log = [
+            "%s: %.5f (ms)" % (pass_raw_log['name'], (pass_raw_log['running_time']) * 1000) for
+            pass_raw_log in passmanager.property_set['pass_raw_log']]
+        self.assertEqual(passmanager.property_set['pass_log'], formatted_log)
+
+    def test_passes(self):
+        """Dump passes in different FlowControllerLinear"""
+        passmanager = PassManager()
+        passmanager.log_passes = True
+        passmanager.append(PassC_TP_RA_PA())
+        passmanager.append(PassB_TP_RA_PA())
+
+        self.assertPassLog(passmanager, ['PassA_TP_NR_NP',
+                                         'PassC_TP_RA_PA',
+                                         'PassB_TP_RA_PA'])
+
+    def test_passes_in_linear(self):
+        """Dump passes in the same FlowControllerLinear"""
+        passmanager = PassManager(passes=[
+            PassC_TP_RA_PA(),
+            PassB_TP_RA_PA(),
+            PassD_TP_NR_NP(argument1=[1, 2]),
+            PassB_TP_RA_PA()])
+        passmanager.log_passes = True
+
+        self.assertPassLog(passmanager, ['PassA_TP_NR_NP',
+                                         'PassC_TP_RA_PA',
+                                         'PassB_TP_RA_PA',
+                                         'PassD_TP_NR_NP',
+                                         'PassA_TP_NR_NP',
+                                         'PassB_TP_RA_PA'])
+
+    def test_control_flow_plugin(self):
+        """ Dump passes in a custom flow controller. """
+        passmanager = PassManager()
+        FlowController.add_flow_controller('do_x_times', DoXTimesController)
+        passmanager.append([PassB_TP_RA_PA(), PassC_TP_RA_PA()], do_x_times=lambda x: 3)
+        passmanager.log_passes = True
+        self.assertPassLog(passmanager, ['PassA_TP_NR_NP',
+                                         'PassB_TP_RA_PA',
+                                         'PassC_TP_RA_PA',
+                                         'PassB_TP_RA_PA',
+                                         'PassC_TP_RA_PA',
+                                         'PassB_TP_RA_PA',
+                                         'PassC_TP_RA_PA'])
+
+    def test_conditional_and_loop(self):
+        """ Dump passes with a conditional and a loop"""
+        passmanager = PassManager()
+        passmanager.append(PassE_AP_NR_NP(True))
+        passmanager.append(
+            [PassK_check_fixed_point_property(),
+             PassA_TP_NR_NP(),
+             PassF_reduce_dag_property()],
+            do_while=lambda property_set: not property_set['property_fixed_point'],
+            condition=lambda property_set: property_set['property_fixed_point'])
+        passmanager.log_passes = True
+        self.assertPassLog(passmanager, ['PassE_AP_NR_NP'])
+
+
 class TestPassManagerReuse(SchedulerTestCase):
     """ The PassManager instance should be resusable"""
+
     def setUp(self):
         self.passmanager = PassManager()
         self.circuit = QuantumCircuit(QuantumRegister(1))