-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
bpo-32591: Add native coroutine origin tracking #5250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
091dc24
d504810
5d8f591
82d0c3e
6c7f73a
7738cc4
2157af3
b0e52ec
c0feb3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import contextlib | ||
| from contextlib import contextmanager, closing | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep |
||
| import copy | ||
| import inspect | ||
| import pickle | ||
|
|
@@ -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") | ||
|
|
@@ -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): | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| return exc_state_traverse(&gen->gi_exc_state, visit, arg); | ||
| } | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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 */ | ||
| }; | ||
|
|
||
|
|
@@ -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 { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, per PEP 7: if ( ... ) {
}
else {
}
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I'd like this whole |
||
| PyObject *origin = PyList_New(depth); | ||
| /* Immediately pass ownership to coro, so on error paths we don't have | ||
| to worry about it separately. */ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ((PyCoroObject *)coro)->cr_origin = origin; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just realized that exposing list object directly through
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another option would be to keep things as is, but to change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I originally left out the 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 |
||
| 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we call |
||
| Py_DECREF(coro); | ||
| return NULL; | ||
| } | ||
| } | ||
|
|
||
| return coro; | ||
| } | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
| int old_depth = tstate->coroutine_origin_tracking_depth; | ||
| tstate->coroutine_origin_tracking_depth = new_depth; | ||
| return old_depth; | ||
| } | ||
|
|
||
| void | ||
| _PyEval_SetCoroutineWrapper(PyObject *wrapper) | ||
| { | ||
|
|
||
| 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]*/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
|
@@ -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) | ||
| { | ||
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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_coroutine_wrapper", sys_set_coroutine_wrapper, METH_O, | ||
| set_coroutine_wrapper_doc}, | ||
| {"get_coroutine_wrapper", sys_get_coroutine_wrapper, METH_NOARGS, | ||
|
|
||
There was a problem hiding this comment.
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_depthdocumentation snippet?There was a problem hiding this comment.
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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work: http://docutils.sourceforge.net/docs/user/rst/quickref.html#hyperlink-targets ?