Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add coro.cr_origin and sys.set_coroutine_origin_tracking_depth
  • Loading branch information
njsmith committed Jan 20, 2018
commit 091dc24c365786131bbe056287a7dae1219fccd5
7 changes: 7 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ attributes:
+-----------+-------------------+---------------------------+
| | cr_code | code |
+-----------+-------------------+---------------------------+
| | cr_origin | where coroutine was |
| | | created, if coroutine |
| | | origin tracking is enabled|
+-----------+-------------------+---------------------------+
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a link to the set_coroutine_origin_tracking_depth documentation snippet?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, that was my original thought. The problem is that

:func:`set_coroutine_origin_tracking_depth`

is too long to fit in the ascii-art box. So... either we need a shorter name for the function, or we need to redraw this whole giant table, and I couldn't think of a satisfactory way to do either in the 2 minutes I spent thinking about it :-). Any suggestions?

I guess we could use that weird ReST substitution thing? I'm not sure how that works.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

| builtin | __doc__ | documentation string |
+-----------+-------------------+---------------------------+
| | __name__ | original name of this |
Expand All @@ -234,6 +238,9 @@ attributes:
The ``__name__`` attribute of generators is now set from the function
name, instead of the code name, and it can now be modified.

.. versionchanged:: 3.7

Add ``cr_origin`` attribute to coroutines.

.. function:: getmembers(object[, predicate])

Expand Down
21 changes: 21 additions & 0 deletions Doc/library/sys.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,27 @@ always available.
This function has been added on a provisional basis (see :pep:`411`
for details.)

.. function:: set_coroutine_origin_tracking_depth(depth)

Allows enabling or disabling coroutine origin tracking. When
enabled, the ``cr_origin`` attribute on coroutine objects will
contain a list of (filename, line number, function name) tuples
describing the traceback where the coroutine object was created.
When disabled, ``cr_origin`` will be None.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to specify how the list is ordered.


To enable, pass a *depth* value greater than zero; this sets the
number of frames whose information will be captured. To disable,
pass set *depth* to zero.

Returns the old value of *depth*.

This setting is thread-local.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thread-local -> thread-specific


.. versionadded:: 3.7

.. note::
This function has been added on a provisional basis (see :pep:`411`
for details.) Use it only for debugging purposes.

.. function:: set_coroutine_wrapper(wrapper)

Expand Down
1 change: 1 addition & 0 deletions Include/ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ PyAPI_FUNC(PyObject *) PyEval_CallMethod(PyObject *obj,
#ifndef Py_LIMITED_API
PyAPI_FUNC(void) PyEval_SetProfile(Py_tracefunc, PyObject *);
PyAPI_FUNC(void) PyEval_SetTrace(Py_tracefunc, PyObject *);
PyAPI_FUNC(int) _PyEval_SetCoroutineOriginTrackingDepth(int new_depth);
PyAPI_FUNC(void) _PyEval_SetCoroutineWrapper(PyObject *);
PyAPI_FUNC(PyObject *) _PyEval_GetCoroutineWrapper(void);
PyAPI_FUNC(void) _PyEval_SetAsyncGenFirstiter(PyObject *);
Expand Down
1 change: 1 addition & 0 deletions Include/genobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
#ifndef Py_LIMITED_API
typedef struct {
_PyGenObject_HEAD(cr)
PyObject *cr_origin;
} PyCoroObject;

PyAPI_DATA(PyTypeObject) PyCoro_Type;
Expand Down
2 changes: 2 additions & 0 deletions Include/pystate.h
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ typedef struct _ts {
void (*on_delete)(void *);
void *on_delete_data;

int coroutine_origin_tracking_depth;

PyObject *coroutine_wrapper;
int in_coroutine_wrapper;

Expand Down
49 changes: 47 additions & 2 deletions Lib/test/test_coroutines.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import contextlib
from contextlib import contextmanager, closing
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep import contextlib please.

import copy
import inspect
import pickle
Expand Down Expand Up @@ -58,7 +58,7 @@ def run_async__await__(coro):
return buffer, result


@contextlib.contextmanager
@contextmanager
def silence_coro_gc():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
Expand Down Expand Up @@ -2041,6 +2041,51 @@ def wrap(gen):
sys.set_coroutine_wrapper(None)


class OriginTrackingTest(unittest.TestCase):
def here(self):
info = inspect.getframeinfo(inspect.currentframe().f_back)
return (info.filename, info.lineno)

def test_origin_tracking(self):
orig_depth = sys.set_coroutine_origin_tracking_depth(0)
try:
async def corofn():
pass

with closing(corofn()) as coro:
self.assertIsNone(coro.cr_origin)

self.assertEqual(sys.set_coroutine_origin_tracking_depth(1), 0)

fname, lineno = self.here()
with closing(corofn()) as coro:
self.assertEqual(coro.cr_origin,
[(fname, lineno + 1, "test_origin_tracking")])

self.assertEqual(sys.set_coroutine_origin_tracking_depth(2), 1)
def nested():
return (self.here(), corofn())
fname, lineno = self.here()
((nested_fname, nested_lineno), coro) = nested()
with closing(coro):
self.assertEqual(coro.cr_origin,
[(nested_fname, nested_lineno, "nested"),
(fname, lineno + 1, "test_origin_tracking")])

# Check we handle running out of frames correctly
sys.set_coroutine_origin_tracking_depth(1000)
with closing(corofn()) as coro:
self.assertTrue(2 < len(coro.cr_origin) < 1000)

# We can't set depth negative
with self.assertRaises(ValueError):
sys.set_coroutine_origin_tracking_depth(-1)
# And trying leaves it unchanged
self.assertEqual(sys.set_coroutine_origin_tracking_depth(0), 1000)

finally:
sys.set_coroutine_origin_tracking_depth(orig_depth)

@support.cpython_only
class CAPITest(unittest.TestCase):

Expand Down
47 changes: 46 additions & 1 deletion Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ gen_traverse(PyGenObject *gen, visitproc visit, void *arg)
Py_VISIT(gen->gi_code);
Py_VISIT(gen->gi_name);
Py_VISIT(gen->gi_qualname);
if (((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE)
Py_VISIT(((PyCoroObject *)gen)->cr_origin);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you only store ints and strings in tuples of that list (without cycles), you can skip the Py_VISIT call.

return exc_state_traverse(&gen->gi_exc_state, visit, arg);
}

Expand Down Expand Up @@ -137,6 +139,8 @@ gen_dealloc(PyGenObject *gen)
gen->gi_frame->f_gen = NULL;
Py_CLEAR(gen->gi_frame);
}
if (((PyCodeObject *)gen->gi_code)->co_flags & CO_COROUTINE)
Py_CLEAR(((PyCoroObject *)gen)->cr_origin);
Py_CLEAR(gen->gi_code);
Py_CLEAR(gen->gi_name);
Py_CLEAR(gen->gi_qualname);
Expand Down Expand Up @@ -990,6 +994,7 @@ static PyMemberDef coro_memberlist[] = {
{"cr_frame", T_OBJECT, offsetof(PyCoroObject, cr_frame), READONLY},
{"cr_running", T_BOOL, offsetof(PyCoroObject, cr_running), READONLY},
{"cr_code", T_OBJECT, offsetof(PyCoroObject, cr_code), READONLY},
{"cr_origin", T_OBJECT, offsetof(PyCoroObject, cr_origin), READONLY},
{NULL} /* Sentinel */
};

Expand Down Expand Up @@ -1161,7 +1166,47 @@ PyTypeObject _PyCoroWrapper_Type = {
PyObject *
PyCoro_New(PyFrameObject *f, PyObject *name, PyObject *qualname)
{
return gen_new_with_qualname(&PyCoro_Type, f, name, qualname);
PyObject *coro = gen_new_with_qualname(&PyCoro_Type, f, name, qualname);
if (!coro)
return NULL;

PyThreadState *tstate = PyThreadState_GET();
int depth = tstate->coroutine_origin_tracking_depth;

if (depth == 0) {
((PyCoroObject *)coro)->cr_origin = NULL;
} else {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, per PEP 7:

if ( ... ) {
}
else {
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I'd like this whole else branch to be in a separate helper function.

PyObject *origin = PyList_New(depth);
/* Immediately pass ownership to coro, so on error paths we don't have
to worry about it separately. */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is neat, but it makes the code harder to read and it's not how we usually do it in CPython. While reviewing the code I've already added 2 comments asking you to add Py_DECREF(origin). Please do it the long way: create, populate, cleanup on errors, and finally assign it to cr_origin. After all it's just two places where an extra decref is needed.

((PyCoroObject *)coro)->cr_origin = origin;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that exposing list object directly through cr_origin means that users can mutate it. How about we cycle through the frames two times: 1st time to determine the real depth we can capture; 2nd time to populate a cr_origin tuple (not list). Traversing a relatively short chain of frames should be pretty fast, and this is a debug mode after all. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another option would be to keep things as is, but to change cr_origin to a getter, and return a copy of the list every time. But I like the tuple idea more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I originally left out the Py_VISIT call and then realized that mutation was possible, so figured I'd put it in just in case :-).

I don't think it matters too much whether we expose a tuple, copy the list, or just keep the current thing where someone can mutate it if they really want to. (The warnings code is robust against such shenanigans: worst case it'll print an error b/c cr_origin was corrupt, and then the real warning without a traceback.) Given this I have a mild preference not to do the tuple thing, because it leads to the most complicated C code. Doing an extra loop is totally doable of course, but it's also plenty complicated to hide some kind of off-by-one bug or something.

PyFrameObject *frame = PyEval_GetFrame();
int i = 0;
for (; i < depth; ++i) {
if (!frame)
break;

PyObject *frameinfo = Py_BuildValue(
"OiO",
frame->f_code->co_filename,
PyFrame_GetLineNumber(frame),
frame->f_code->co_name);
if (!frameinfo) {
Py_DECREF(coro);
return NULL;
}
PyList_SET_ITEM(origin, i, frameinfo);

frame = frame->f_back;
}
/* Truncate the list if necessary */
if (PyList_SetSlice(origin, i, depth, NULL) < 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call PyList_SetSlice only when needed?

Py_DECREF(coro);
return NULL;
}
}

return coro;
}


Expand Down
9 changes: 9 additions & 0 deletions Python/ceval.c
Original file line number Diff line number Diff line change
Expand Up @@ -4387,6 +4387,15 @@ PyEval_SetTrace(Py_tracefunc func, PyObject *arg)
|| (tstate->c_profilefunc != NULL));
}

int
_PyEval_SetCoroutineOriginTrackingDepth(int new_depth)
{
PyThreadState *tstate = PyThreadState_GET();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add assert(new_depth >= 0) for cases when somebody uses the C API directly (sys.set_coroutine_origin_tracking_depth has a check for Python users).

int old_depth = tstate->coroutine_origin_tracking_depth;
tstate->coroutine_origin_tracking_depth = new_depth;
return old_depth;
}

void
_PyEval_SetCoroutineWrapper(PyObject *wrapper)
{
Expand Down
43 changes: 43 additions & 0 deletions Python/clinic/sysmodule.c.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*[clinic input]
preserve
[clinic start generated code]*/

PyDoc_STRVAR(sys_set_coroutine_origin_tracking_depth__doc__,
"set_coroutine_origin_tracking_depth($module, /, depth)\n"
"--\n"
"\n"
"Enable or disable origin tracking for coroutine objects in this thread.\n"
"\n"
"Coroutine objects will track \'depth\' frames of traceback information about\n"
"where they came from, available in their cr_origin attribute. Set depth of 0\n"
"to disable. Returns old value.");

#define SYS_SET_COROUTINE_ORIGIN_TRACKING_DEPTH_METHODDEF \
{"set_coroutine_origin_tracking_depth", (PyCFunction)sys_set_coroutine_origin_tracking_depth, METH_FASTCALL|METH_KEYWORDS, sys_set_coroutine_origin_tracking_depth__doc__},

static int
sys_set_coroutine_origin_tracking_depth_impl(PyObject *module, int depth);

static PyObject *
sys_set_coroutine_origin_tracking_depth(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
static const char * const _keywords[] = {"depth", NULL};
static _PyArg_Parser _parser = {"i:set_coroutine_origin_tracking_depth", _keywords, 0};
int depth;
int _return_value;

if (!_PyArg_ParseStackAndKeywords(args, nargs, kwnames, &_parser,
&depth)) {
goto exit;
}
_return_value = sys_set_coroutine_origin_tracking_depth_impl(module, depth);
if ((_return_value == -1) && PyErr_Occurred()) {
goto exit;
}
return_value = PyLong_FromLong((long)_return_value);

exit:
return return_value;
}
/*[clinic end generated code: output=9448b0765e4d5bf0 input=a9049054013a1b77]*/
2 changes: 2 additions & 0 deletions Python/pystate.c
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ new_threadstate(PyInterpreterState *interp, int init)
tstate->on_delete = NULL;
tstate->on_delete_data = NULL;

tstate->coroutine_origin_tracking_depth = 0;

tstate->coroutine_wrapper = NULL;
tstate->in_coroutine_wrapper = 0;

Expand Down
31 changes: 31 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ extern void *PyWin_DLLhModule;
extern const char *PyWin_DLLVersionString;
#endif

/*[clinic input]
module sys
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=3726b388feee8cea]*/

#include "clinic/sysmodule.c.h"

_Py_IDENTIFIER(_);
_Py_IDENTIFIER(__sizeof__);
_Py_IDENTIFIER(_xoptions);
Expand Down Expand Up @@ -710,6 +717,29 @@ sys_setrecursionlimit(PyObject *self, PyObject *args)
Py_RETURN_NONE;
}

/*[clinic input]
sys.set_coroutine_origin_tracking_depth -> int

depth: int

Enable or disable origin tracking for coroutine objects in this thread.

Coroutine objects will track 'depth' frames of traceback information about
where they came from, available in their cr_origin attribute. Set depth of 0
to disable. Returns old value.
[clinic start generated code]*/

static int
sys_set_coroutine_origin_tracking_depth_impl(PyObject *module, int depth)
/*[clinic end generated code: output=1d421106d83a2fce input=f0a48609fc463a5c]*/
{
if (depth < 0) {
PyErr_SetString(PyExc_ValueError, "depth must be >= 0");
return -1;
}
return _PyEval_SetCoroutineOriginTrackingDepth(depth);
}

static PyObject *
sys_set_coroutine_wrapper(PyObject *self, PyObject *wrapper)
{
Expand Down Expand Up @@ -1512,6 +1542,7 @@ static PyMethodDef sys_methods[] = {
{"call_tracing", sys_call_tracing, METH_VARARGS, call_tracing_doc},
{"_debugmallocstats", sys_debugmallocstats, METH_NOARGS,
debugmallocstats_doc},
SYS_SET_COROUTINE_ORIGIN_TRACKING_DEPTH_METHODDEF
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add sys.get_coroutine_origin_tracking_depth?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, I guess. I was thinking this is sufficiently arcane functionality that I could be lazy and get away with the set-returns-the-old-value pattern (cf. signal.set_wakeup_fd), but you're probably right that set+get is a nicer API.

{"set_coroutine_wrapper", sys_set_coroutine_wrapper, METH_O,
set_coroutine_wrapper_doc},
{"get_coroutine_wrapper", sys_get_coroutine_wrapper, METH_NOARGS,
Expand Down