Faster Python stepping using a watchpoint approach (f->f_lasti)
authorMark Florisson <markflorisson88@gmail.com>
Tue, 14 Dec 2010 00:12:49 +0000 (01:12 +0100)
committerMark Florisson <markflorisson88@gmail.com>
Tue, 14 Dec 2010 00:12:49 +0000 (01:12 +0100)
Cython/Debugger/Tests/test_libcython_in_gdb.py
Cython/Debugger/libcython.py
Cython/Debugger/libpython.py

index e8827b66d6e3cfc1b387fb990dd45116da886f34..d8a09f6aecd56875ac71b85e35a59db80a47bcde 100644 (file)
@@ -262,7 +262,7 @@ class TestBacktrace(DebugTestCase):
         
         gdb.execute('cy bt')
         result = gdb.execute('cy bt -a', to_string=True)
-        assert re.search(r'\#0 *0x.* in main\(\) at', result), result
+        assert re.search(r'\#0 *0x.* in main\(\)', result), result
 
 
 class TestFunctions(DebugTestCase):
index 1a1a41729ea65f192441ee8694d6c95563093b9a..4846235530fdf96ec4a9152f27e7c7a990029465 100644 (file)
@@ -872,8 +872,18 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
         result.extend(self.cy.functions_by_cname)
         return result
 
+
+class CyStep(CythonCodeStepper):
+    "Step through Cython, Python or C code."
+    
+    name = 'cy step'
+    stepinto = True
+    
     def invoke(self, args, from_tty):
-        if not self.is_cython_function() and not self.is_python_function():
+        if self.is_python_function():
+            libpython.py_step.get_source_line = self.get_source_line
+            libpython.py_step.invoke(args, from_tty)
+        elif not self.is_cython_function():
             if self.stepinto:
                 command = 'step'
             else:
@@ -884,14 +894,7 @@ class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
             self.step()
 
 
-class CyStep(CythonCodeStepper):
-    "Step through Cython, Python or C code."
-    
-    name = 'cy step'
-    stepinto = True
-
-
-class CyNext(CythonCodeStepper):
+class CyNext(CyStep):
     "Step-over Python code."
 
     name = 'cy next'
index b289d04bb2c2a602ef78c7bbd57f691f1af4eda1..8e9ec2afc202aa83fb9cac338a248d8a2fe3200e 100644 (file)
@@ -1715,6 +1715,8 @@ class PyNameEquals(gdb.Function):
         if frame.is_evalframeex():
             pyframe = frame.get_pyop()
             if pyframe is None:
+                warnings.warn("Use a Python debug build, Python breakpoints "
+                              "won't work otherwise.")
                 return None
             
             return getattr(pyframe, attr).proxyval(set())
@@ -1846,7 +1848,8 @@ class GenericCodeStepper(gdb.Command):
     methods:
         
         lineno(frame)
-            tells the current line number (only called for a relevant frame)
+            Tells the current line number (only called for a relevant frame).
+            If lineno is a false value it is not checked for a difference.
         
         is_relevant_function(frame)
             tells whether we care about frame 'frame'
@@ -1956,7 +1959,11 @@ class GenericCodeStepper(gdb.Command):
         elif get_selected_inferior().pid == 0:
             return result
         else:
-            return None
+            match = re.search('.*[Ww]arning.*', result, re.MULTILINE)
+            if match:
+                print match.group(0)
+
+            return ''
         
     def _stackdepth(self, frame):
         depth = 0
@@ -1966,13 +1973,13 @@ class GenericCodeStepper(gdb.Command):
             
         return depth
     
-    def finish_executing(self, result):
+    def finish_executing(self, output):
         """
         After doing some kind of code running in the inferior, print the line
         of source code or the result of the last executed gdb command (passed
         in as the `result` argument).
         """
-        result = self.stopped(result)
+        result = self.stopped(output)
         if result:
             print result.strip()
             # check whether the program was killed by a signal, it should still
@@ -1982,7 +1989,13 @@ class GenericCodeStepper(gdb.Command):
             except RuntimeError:
                 pass
             else:
-                print self.get_source_line(frame)
+                line = self.get_source_line(frame)
+                
+                if line is None:
+                    print output
+                else:
+                    print result
+                    print line
         else:
             frame = gdb.selected_frame()
             output = None
@@ -2024,33 +2037,53 @@ class GenericCodeStepper(gdb.Command):
                 break
             
             hitbp = re.search(r'Breakpoint (\d+)', result)
-            is_relavant = self.is_relevant_function(frame)
-            if hitbp or is_relavant or self.stopped(result):
+            is_relevant = self.is_relevant_function(frame)
+            if hitbp or is_relevant or self.stopped(result):
                 break
             
         self.finish_executing(result)
     
-    def _step(self):
+    def step(self, stepover_command='next'):
         """
         Do a single step or step-over. Returns the result of the last gdb
         command that made execution stop.
+        
+        This implementation, for stepping, sets (conditional) breakpoints for 
+        all functions that are deemed relevant. It then does a step over until
+        either something halts execution, or until the next line is reached.
+        
+        If, however, stepover_command is given, it should be a string gdb 
+        command that continues execution in some way. The idea is that the 
+        caller has set a (conditional) breakpoint or watchpoint that can work 
+        more efficiently than the step-over loop. For Python this means setting
+        a watchpoint for f->f_lasti, which means we can then subsequently 
+        "finish" frames.
+        We want f->f_lasti instead of f->f_lineno, because the latter only 
+        works properly with local trace functions, see 
+        PyFrameObjectPtr.current_line_num and PyFrameObjectPtr.addr2line.
         """
         if self.stepinto:
             self.enable_breakpoints()
         
         beginframe = gdb.selected_frame()
-        beginline = self.lineno(beginframe)
-        if not self.stepinto:
-            depth = self._stackdepth(beginframe)
+        
+        if self.is_relevant_function(beginframe):
+            # If we start in a relevant frame, initialize stuff properly. If
+            # we don't start in a relevant frame, the loop will halt 
+            # immediately. So don't call self.lineno() as it may raise for
+            # irrelevant frames.
+            beginline = self.lineno(beginframe)
+            
+            if not self.stepinto:
+                depth = self._stackdepth(beginframe)
         
         newframe = beginframe
-        result = ''
-
+        
         while True:
             if self.is_relevant_function(newframe):
-                result = gdb.execute('next', to_string=True)
+                result = gdb.execute(stepover_command, to_string=True)
             else:
-                result = self._finish()
+                self.finish()
             
             if self.stopped(result):
                 break
@@ -2065,7 +2098,8 @@ class GenericCodeStepper(gdb.Command):
             m = re.search(r'Breakpoint (\d+)', result)
             if m:
                 bp = self.runtime_breakpoints.get(framename)
-                if bp is None or (m.group(1) == bp and is_relevant_function):
+                if (bp is None or 
+                    (is_relevant_function and bp == m.group(1))):
                     # although we hit a breakpoint, we still need to check
                     # that the function, in case hit by a runtime breakpoint,
                     # is in the right context
@@ -2083,17 +2117,17 @@ class GenericCodeStepper(gdb.Command):
                 if is_relevant_function:
                     break
             else:
-                if self.lineno(newframe) > beginline:
+                # newframe equals beginframe, check for a difference in the
+                # line number
+                lineno = self.lineno(newframe)
+                if lineno and lineno != beginline:
                     break
         
         if self.stepinto:
             self.disable_breakpoints()
 
-        return result
+        self.finish_executing(result)
     
-    def step(self, *args):
-        return self.finish_executing(self._step())
-
     def run(self, *args):
         self.finish_executing(gdb.execute('run', to_string=True))
     
@@ -2108,7 +2142,7 @@ class PythonCodeStepper(GenericCodeStepper):
         if pyframe:
             return pyframe
         else:
-            raise gdb.GdbError(
+            raise gdb.RuntimeError(
                 "Unable to find the Python frame, run your code with a debug "
                 "build (configure with --with-pydebug or compile with -g).")
         
@@ -2133,7 +2167,32 @@ class PythonCodeStepper(GenericCodeStepper):
 class PyStep(PythonCodeStepper):
     "Step through Python code."
     
-    invoke = PythonCodeStepper.step
+    def __init__(self, *args, **kwargs):
+        super(PyStep, self).__init__(*args, **kwargs)
+        self.lastframe = None
+    
+    def invoke(self, args, from_tty):
+        # Set a watchpoint for a frame once as deleting it will make py-step
+        # unrepeatable. 
+        # See http://sourceware.org/bugzilla/show_bug.cgi?id=12216
+        # When the watchpoint goes out of scope it will automatically 
+        # disappear.
+        
+        newframe = gdb.selected_frame()
+        framewrapper = Frame(newframe)
+        if newframe != self.lastframe and framewrapper.is_evalframeex():
+            self.lastframe = newframe
+            output = gdb.execute('watch f->f_lasti', to_string=True)
+            
+        self.step(stepover_command='py-finish')
+        
+        # match = re.search(r'[Ww]atchpoint (\d+):', output)
+        # if match:
+            # watchpoint = match.group(1)
+            # gdb.execute('delete %s' % watchpoint)
+    
+    def invoke(self, args, from_tty):
+        self.step()
     
 class PyNext(PythonCodeStepper):
     "Step-over Python code."