]> granicus.if.org Git - python/commitdiff
After approval from Anthony, merge the tim-current_frames
authorTim Peters <tim.peters@gmail.com>
Mon, 10 Jul 2006 21:08:24 +0000 (21:08 +0000)
committerTim Peters <tim.peters@gmail.com>
Mon, 10 Jul 2006 21:08:24 +0000 (21:08 +0000)
branch into the trunk.  This adds a new sys._current_frames()
function, which returns a dict mapping thread id to topmost
thread stack frame.

Doc/lib/libsys.tex
Include/pystate.h
Lib/test/test_sys.py
Misc/NEWS
Python/pystate.c
Python/sysmodule.c

index 686e50e7eb5e83b8da7a489fe58340608d256984..d55aac3eff587325d4d25841893831a922bf63bd 100644 (file)
@@ -41,7 +41,7 @@ It is always available.
   \code{Include/patchlevel.h} if the branch is a tag. Otherwise,
   it is \code{None}.
   \versionadded{2.5}
-\end{datadesc}  
+\end{datadesc}
 
 \begin{datadesc}{builtin_module_names}
   A tuple of strings giving the names of all modules that are compiled
@@ -55,6 +55,23 @@ It is always available.
   interpreter.
 \end{datadesc}
 
+\begin{funcdesc}{_current_frames}{}
+  Return a dictionary mapping each thread's identifier to the topmost stack
+  frame currently active in that thread at the time the function is called.
+  Note that functions in the \refmodule{traceback} module can build the
+  call stack given such a frame.
+
+  This is most useful for debugging deadlock:  this function does not
+  require the deadlocked threads' cooperation, and such threads' call stacks
+  are frozen for as long as they remain deadlocked.  The frame returned
+  for a non-deadlocked thread may bear no relationship to that thread's
+  current activity by the time calling code examines the frame.
+
+  This function should be used for internal and specialized purposes
+  only.
+  \versionadded{2.5}
+\end{funcdesc}
+
 \begin{datadesc}{dllhandle}
   Integer specifying the handle of the Python DLL.
   Availability: Windows.
@@ -142,7 +159,7 @@ It is always available.
   function, \function{exc_info()} will return three \code{None} values until
   another exception is raised in the current thread or the execution stack
   returns to a frame where another exception is being handled.
-  
+
   This function is only needed in only a few obscure situations.  These
   include logging and error handling systems that report information on the
   last or current exception.  This function can also be used to try to free
@@ -241,7 +258,7 @@ It is always available.
 \begin{itemize}
 \item On Windows 9x, the encoding is ``mbcs''.
 \item On Mac OS X, the encoding is ``utf-8''.
-\item On Unix, the encoding is the user's preference 
+\item On Unix, the encoding is the user's preference
       according to the result of nl_langinfo(CODESET), or None if
       the nl_langinfo(CODESET) failed.
 \item On Windows NT+, file names are Unicode natively, so no conversion
@@ -279,8 +296,8 @@ It is always available.
 \end{funcdesc}
 
 \begin{funcdesc}{getwindowsversion}{}
-  Return a tuple containing five components, describing the Windows 
-  version currently running.  The elements are \var{major}, \var{minor}, 
+  Return a tuple containing five components, describing the Windows
+  version currently running.  The elements are \var{major}, \var{minor},
   \var{build}, \var{platform}, and \var{text}.  \var{text} contains
   a string while all other values are integers.
 
@@ -491,7 +508,7 @@ else:
   be registered using \function{settrace()} for each thread being
   debugged.  \note{The \function{settrace()} function is intended only
   for implementing debuggers, profilers, coverage tools and the like.
-  Its behavior is part of the implementation platform, rather than 
+  Its behavior is part of the implementation platform, rather than
   part of the language definition, and thus may not be available in
   all Python implementations.}
 \end{funcdesc}
index bfd3548fe2d6cdae1149c2445b4552c1604f2228..cf2969596310499f617c0a6abea1d69063635782 100644 (file)
@@ -171,6 +171,11 @@ PyAPI_FUNC(void) PyGILState_Release(PyGILState_STATE);
 */
 PyAPI_FUNC(PyThreadState *) PyGILState_GetThisThreadState(void);
 
+/* The implementation of sys._current_frames()  Returns a dict mapping
+   thread id to that thread's current frame.
+*/
+PyAPI_FUNC(PyObject *) _PyThread_CurrentFrames(void);
+
 /* Routines for advanced debuggers, requested by David Beazley.
    Don't use unless you know what you are doing! */
 PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Head(void);
index ae2a1c81e25afed2dc40ce8d2ee5a9b7784bbf2d..bb86c882377694a06b5601c6e3721f215504a915 100644 (file)
@@ -237,6 +237,67 @@ class SysModuleTest(unittest.TestCase):
             is sys._getframe().f_code
         )
 
+    # sys._current_frames() is a CPython-only gimmick.
+    def test_current_frames(self):
+        import threading, thread
+        import traceback
+
+        # Spawn a thread that blocks at a known place.  Then the main
+        # thread does sys._current_frames(), and verifies that the frames
+        # returned make sense.
+        entered_g = threading.Event()
+        leave_g = threading.Event()
+        thread_info = []  # the thread's id
+
+        def f123():
+            g456()
+
+        def g456():
+            thread_info.append(thread.get_ident())
+            entered_g.set()
+            leave_g.wait()
+
+        t = threading.Thread(target=f123)
+        t.start()
+        entered_g.wait()
+
+        # At this point, t has finished its entered_g.set(), and is blocked
+        # in its leave_g.wait().
+        self.assertEqual(len(thread_info), 1)
+        thread_id = thread_info[0]
+
+        d = sys._current_frames()
+
+        main_id = thread.get_ident()
+        self.assert_(main_id in d)
+        self.assert_(thread_id in d)
+
+        # Verify that the captured main-thread frame is _this_ frame.
+        frame = d.pop(main_id)
+        self.assert_(frame is sys._getframe())
+
+        # Verify that the captured thread frame is blocked in g456, called
+        # from f123.  This is a litte tricky, since various bits of
+        # threading.py are also in the thread's call stack.
+        frame = d.pop(thread_id)
+        stack = traceback.extract_stack(frame)
+        for i, (filename, lineno, funcname, sourceline) in enumerate(stack):
+            if funcname == "f123":
+                break
+        else:
+            self.fail("didn't find f123() on thread's call stack")
+
+        self.assertEqual(sourceline, "g456()")
+
+        # And the next record must be for g456().
+        filename, lineno, funcname, sourceline = stack[i+1]
+        self.assertEqual(funcname, "g456")
+        self.assertEqual(sourceline, "leave_g.wait()")
+
+        # Reap the spawned thread.
+        leave_g.set()
+        t.join()
+
     def test_attributes(self):
         self.assert_(isinstance(sys.api_version, int))
         self.assert_(isinstance(sys.argv, list))
index 3add040e9b8c4c9bd93bc7fb84dbd81cfc6bf687..770b87f02bb65ad3f0d131cd452a438de2d7b5c9 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -36,10 +36,17 @@ Core and builtins
 - Bug #1512814, Fix incorrect lineno's when code at module scope
   started after line 256.
 
+- New function ``sys._current_frames()`` returns a dict mapping thread
+  id to topmost thread stack frame.  This is for expert use, and is
+  especially useful for debugging application deadlocks.  The functionality
+  was previously available in Fazal Majid's ``threadframe`` extension
+  module, but it wasn't possible to do this in a wholly threadsafe way from
+  an extension.
+
 Library
 -------
 
-- Bug #1508010: msvccompiler now requires the DISTUTILS_USE_SDK 
+- Bug #1508010: msvccompiler now requires the DISTUTILS_USE_SDK
   environment variable to be set in order to the SDK environment
   for finding the compiler, include files, etc.
 
@@ -126,7 +133,7 @@ Extension Modules
 Build
 -----
 
-- 'configure' now detects the zlib library the same way as distutils. 
+- 'configure' now detects the zlib library the same way as distutils.
   Previously, the slight difference could cause compilation errors of the
   'zlib' module on systems with more than one version of zlib.
 
index b8f460ff8a841faec72aac2bc6d3e9000e24c486..b872dc0752c6b231e400e972e45d944fe6255782 100644 (file)
@@ -444,15 +444,15 @@ _PyGILState_NoteThreadState(PyThreadState* tstate)
        /* If autoTLSkey is 0, this must be the very first threadstate created
           in Py_Initialize().  Don't do anything for now (we'll be back here
           when _PyGILState_Init is called). */
-       if (!autoTLSkey) 
+       if (!autoTLSkey)
                return;
-       
+
        /* Stick the thread state for this thread in thread local storage.
 
           The only situation where you can legitimately have more than one
           thread state for an OS level thread is when there are multiple
           interpreters, when:
-              
+
               a) You shouldn't really be using the PyGILState_ APIs anyway,
                  and:
 
@@ -550,6 +550,54 @@ PyGILState_Release(PyGILState_STATE oldstate)
                PyEval_SaveThread();
 }
 
+/* The implementation of sys._current_frames().  This is intended to be
+   called with the GIL held, as it will be when called via
+   sys._current_frames().  It's possible it would work fine even without
+   the GIL held, but haven't thought enough about that.
+*/
+PyObject *
+_PyThread_CurrentFrames(void)
+{
+       PyObject *result;
+       PyInterpreterState *i;
+
+       result = PyDict_New();
+       if (result == NULL)
+               return NULL;
+
+       /* for i in all interpreters:
+        *     for t in all of i's thread states:
+        *          if t's frame isn't NULL, map t's id to its frame
+        * Because these lists can mutute even when the GIL is held, we
+        * need to grab head_mutex for the duration.
+        */
+       HEAD_LOCK();
+       for (i = interp_head; i != NULL; i = i->next) {
+               PyThreadState *t;
+               for (t = i->tstate_head; t != NULL; t = t->next) {
+                       PyObject *id;
+                       int stat;
+                       struct _frame *frame = t->frame;
+                       if (frame == NULL)
+                               continue;
+                       id = PyInt_FromLong(t->thread_id);
+                       if (id == NULL)
+                               goto Fail;
+                       stat = PyDict_SetItem(result, id, (PyObject *)frame);
+                       Py_DECREF(id);
+                       if (stat < 0)
+                               goto Fail;
+               }
+       }
+       HEAD_UNLOCK();
+       return result;
+
+ Fail:
+       HEAD_UNLOCK();
+       Py_DECREF(result);
+       return NULL;
+}
+
 #ifdef __cplusplus
 }
 #endif
index 785653ede0a6fe1bd8c0b70a80ffa765b03b568e..ea1388b6412469ca0d1c33d57eacf9e609a2cebc 100644 (file)
@@ -660,6 +660,21 @@ sys_getframe(PyObject *self, PyObject *args)
        return (PyObject*)f;
 }
 
+PyDoc_STRVAR(current_frames_doc,
+"_current_frames() -> dictionary\n\
+\n\
+Return a dictionary mapping each current thread T's thread id to T's\n\
+current stack frame.\n\
+\n\
+This function should be used for specialized purposes only."
+);
+
+static PyObject *
+sys_current_frames(PyObject *self, PyObject *noargs)
+{
+       return _PyThread_CurrentFrames();
+}
+
 PyDoc_STRVAR(call_tracing_doc,
 "call_tracing(func, args) -> object\n\
 \n\
@@ -722,6 +737,8 @@ static PyMethodDef sys_methods[] = {
        /* Might as well keep this in alphabetic order */
        {"callstats", (PyCFunction)PyEval_GetCallStats, METH_NOARGS,
         callstats_doc},
+       {"_current_frames", sys_current_frames, METH_NOARGS,
+        current_frames_doc},
        {"displayhook", sys_displayhook, METH_O, displayhook_doc},
        {"exc_info",    sys_exc_info, METH_NOARGS, exc_info_doc},
        {"exc_clear",   sys_exc_clear, METH_NOARGS, exc_clear_doc},