Issue #17934: Add a clear() method to frame objects, to help clean up expensive details (local variables) and break reference cycles.

This commit is contained in:
Antoine Pitrou 2013-08-05 23:26:40 +02:00
parent c53204b947
commit 58720d6145
10 changed files with 80 additions and 10 deletions

View File

@ -846,6 +846,10 @@ index of the current line within that list.
finally: finally:
del frame del frame
If you want to keep the frame around (for example to print a traceback
later), you can also break reference cycles by using the
:meth:`frame.clear` method.
The optional *context* argument supported by most of these functions specifies The optional *context* argument supported by most of these functions specifies
the number of lines of context to return, which are centered around the current the number of lines of context to return, which are centered around the current
line. line.

View File

@ -934,6 +934,20 @@ Internal types
frame). A debugger can implement a Jump command (aka Set Next Statement) frame). A debugger can implement a Jump command (aka Set Next Statement)
by writing to f_lineno. by writing to f_lineno.
Frame objects support one method:
.. method:: frame.clear()
This method clears all references to local variables held by the
frame. Also, if the frame belonged to a generator, the generator
is finalized. This helps break reference cycles involving frame
objects (for example when catching an exception and storing its
traceback for later use).
:exc:`RuntimeError` is raised if the frame is currently executing.
.. versionadded:: 3.4
Traceback objects Traceback objects
.. index:: .. index::
object: traceback object: traceback

View File

@ -36,6 +36,8 @@ typedef struct _frame {
non-generator frames. See the save_exc_state and swap_exc_state non-generator frames. See the save_exc_state and swap_exc_state
functions in ceval.c for details of their use. */ functions in ceval.c for details of their use. */
PyObject *f_exc_type, *f_exc_value, *f_exc_traceback; PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
/* Borrowed referenced to a generator, or NULL */
PyObject *f_gen;
PyThreadState *f_tstate; PyThreadState *f_tstate;
int f_lasti; /* Last instruction if called */ int f_lasti; /* Last instruction if called */
@ -46,6 +48,7 @@ typedef struct _frame {
bytecode index. */ bytecode index. */
int f_lineno; /* Current line number */ int f_lineno; /* Current line number */
int f_iblock; /* index in f_blockstack */ int f_iblock; /* index in f_blockstack */
char f_executing; /* whether the frame is still executing */
PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */ PyObject *f_localsplus[1]; /* locals+stack, dynamically sized */
} PyFrameObject; } PyFrameObject;

View File

@ -36,6 +36,8 @@ PyAPI_FUNC(PyObject *) PyGen_New(struct _frame *);
PyAPI_FUNC(int) PyGen_NeedsFinalizing(PyGenObject *); PyAPI_FUNC(int) PyGen_NeedsFinalizing(PyGenObject *);
PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **); PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);
PyObject *_PyGen_Send(PyGenObject *, PyObject *); PyObject *_PyGen_Send(PyGenObject *, PyObject *);
PyAPI_FUNC(void) _PyGen_Finalize(PyObject *self);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@ -764,7 +764,7 @@ class SizeofTest(unittest.TestCase):
nfrees = len(x.f_code.co_freevars) nfrees = len(x.f_code.co_freevars)
extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\ extras = x.f_code.co_stacksize + x.f_code.co_nlocals +\
ncells + nfrees - 1 ncells + nfrees - 1
check(x, vsize('12P3i' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P')) check(x, vsize('13P3ic' + CO_MAXBLOCKS*'3i' + 'P' + extras*'P'))
# function # function
def func(): pass def func(): pass
check(func, size('12P')) check(func, size('12P'))

View File

@ -150,11 +150,17 @@ class SyntaxTracebackCases(unittest.TestCase):
class TracebackFormatTests(unittest.TestCase): class TracebackFormatTests(unittest.TestCase):
def test_traceback_format(self): def some_exception(self):
try:
raise KeyError('blah') raise KeyError('blah')
def check_traceback_format(self, cleanup_func=None):
try:
self.some_exception()
except KeyError: except KeyError:
type_, value, tb = sys.exc_info() type_, value, tb = sys.exc_info()
if cleanup_func is not None:
# Clear the inner frames, not this one
cleanup_func(tb.tb_next)
traceback_fmt = 'Traceback (most recent call last):\n' + \ traceback_fmt = 'Traceback (most recent call last):\n' + \
''.join(traceback.format_tb(tb)) ''.join(traceback.format_tb(tb))
file_ = StringIO() file_ = StringIO()
@ -183,12 +189,22 @@ class TracebackFormatTests(unittest.TestCase):
# Make sure that the traceback is properly indented. # Make sure that the traceback is properly indented.
tb_lines = python_fmt.splitlines() tb_lines = python_fmt.splitlines()
self.assertEqual(len(tb_lines), 3) self.assertEqual(len(tb_lines), 5)
banner, location, source_line = tb_lines banner = tb_lines[0]
location, source_line = tb_lines[-2:]
self.assertTrue(banner.startswith('Traceback')) self.assertTrue(banner.startswith('Traceback'))
self.assertTrue(location.startswith(' File')) self.assertTrue(location.startswith(' File'))
self.assertTrue(source_line.startswith(' raise')) self.assertTrue(source_line.startswith(' raise'))
def test_traceback_format(self):
self.check_traceback_format()
def test_traceback_format_with_cleared_frames(self):
# Check that traceback formatting also works with a clear()ed frame
def cleanup_tb(tb):
tb.tb_frame.clear()
self.check_traceback_format(cleanup_tb)
def test_stack_format(self): def test_stack_format(self):
# Verify _stack functions. Note we have to use _getframe(1) to # Verify _stack functions. Note we have to use _getframe(1) to
# compare them without this frame appearing in the output # compare them without this frame appearing in the output

View File

@ -10,6 +10,9 @@ Projected Release date: 2013-09-08
Core and Builtins Core and Builtins
----------------- -----------------
- Issue #17934: Add a clear() method to frame objects, to help clean up
expensive details (local variables) and break reference cycles.
Library Library
------- -------

View File

@ -488,7 +488,7 @@ frame_traverse(PyFrameObject *f, visitproc visit, void *arg)
} }
static void static void
frame_clear(PyFrameObject *f) frame_tp_clear(PyFrameObject *f)
{ {
PyObject **fastlocals, **p, **oldtop; PyObject **fastlocals, **p, **oldtop;
Py_ssize_t i, slots; Py_ssize_t i, slots;
@ -500,6 +500,7 @@ frame_clear(PyFrameObject *f)
*/ */
oldtop = f->f_stacktop; oldtop = f->f_stacktop;
f->f_stacktop = NULL; f->f_stacktop = NULL;
f->f_executing = 0;
Py_CLEAR(f->f_exc_type); Py_CLEAR(f->f_exc_type);
Py_CLEAR(f->f_exc_value); Py_CLEAR(f->f_exc_value);
@ -519,6 +520,25 @@ frame_clear(PyFrameObject *f)
} }
} }
static PyObject *
frame_clear(PyFrameObject *f)
{
if (f->f_executing) {
PyErr_SetString(PyExc_RuntimeError,
"cannot clear an executing frame");
return NULL;
}
if (f->f_gen) {
_PyGen_Finalize(f->f_gen);
assert(f->f_gen == NULL);
}
frame_tp_clear(f);
Py_RETURN_NONE;
}
PyDoc_STRVAR(clear__doc__,
"F.clear(): clear most references held by the frame");
static PyObject * static PyObject *
frame_sizeof(PyFrameObject *f) frame_sizeof(PyFrameObject *f)
{ {
@ -538,6 +558,8 @@ PyDoc_STRVAR(sizeof__doc__,
"F.__sizeof__() -> size of F in memory, in bytes"); "F.__sizeof__() -> size of F in memory, in bytes");
static PyMethodDef frame_methods[] = { static PyMethodDef frame_methods[] = {
{"clear", (PyCFunction)frame_clear, METH_NOARGS,
clear__doc__},
{"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS, {"__sizeof__", (PyCFunction)frame_sizeof, METH_NOARGS,
sizeof__doc__}, sizeof__doc__},
{NULL, NULL} /* sentinel */ {NULL, NULL} /* sentinel */
@ -566,7 +588,7 @@ PyTypeObject PyFrame_Type = {
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
0, /* tp_doc */ 0, /* tp_doc */
(traverseproc)frame_traverse, /* tp_traverse */ (traverseproc)frame_traverse, /* tp_traverse */
(inquiry)frame_clear, /* tp_clear */ (inquiry)frame_tp_clear, /* tp_clear */
0, /* tp_richcompare */ 0, /* tp_richcompare */
0, /* tp_weaklistoffset */ 0, /* tp_weaklistoffset */
0, /* tp_iter */ 0, /* tp_iter */
@ -708,6 +730,8 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
f->f_lasti = -1; f->f_lasti = -1;
f->f_lineno = code->co_firstlineno; f->f_lineno = code->co_firstlineno;
f->f_iblock = 0; f->f_iblock = 0;
f->f_executing = 0;
f->f_gen = NULL;
_PyObject_GC_TRACK(f); _PyObject_GC_TRACK(f);
return f; return f;

View File

@ -15,8 +15,8 @@ gen_traverse(PyGenObject *gen, visitproc visit, void *arg)
return 0; return 0;
} }
static void void
gen_finalize(PyObject *self) _PyGen_Finalize(PyObject *self)
{ {
PyGenObject *gen = (PyGenObject *)self; PyGenObject *gen = (PyGenObject *)self;
PyObject *res; PyObject *res;
@ -140,6 +140,7 @@ gen_send_ex(PyGenObject *gen, PyObject *arg, int exc)
Py_XDECREF(t); Py_XDECREF(t);
Py_XDECREF(v); Py_XDECREF(v);
Py_XDECREF(tb); Py_XDECREF(tb);
gen->gi_frame->f_gen = NULL;
gen->gi_frame = NULL; gen->gi_frame = NULL;
Py_DECREF(f); Py_DECREF(f);
} }
@ -505,7 +506,7 @@ PyTypeObject PyGen_Type = {
0, /* tp_weaklist */ 0, /* tp_weaklist */
0, /* tp_del */ 0, /* tp_del */
0, /* tp_version_tag */ 0, /* tp_version_tag */
gen_finalize, /* tp_finalize */ _PyGen_Finalize, /* tp_finalize */
}; };
PyObject * PyObject *
@ -517,6 +518,7 @@ PyGen_New(PyFrameObject *f)
return NULL; return NULL;
} }
gen->gi_frame = f; gen->gi_frame = f;
f->f_gen = (PyObject *) gen;
Py_INCREF(f->f_code); Py_INCREF(f->f_code);
gen->gi_code = (PyObject *)(f->f_code); gen->gi_code = (PyObject *)(f->f_code);
gen->gi_running = 0; gen->gi_running = 0;

View File

@ -1182,6 +1182,7 @@ PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
stack_pointer = f->f_stacktop; stack_pointer = f->f_stacktop;
assert(stack_pointer != NULL); assert(stack_pointer != NULL);
f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */ f->f_stacktop = NULL; /* remains NULL unless yield suspends frame */
f->f_executing = 1;
if (co->co_flags & CO_GENERATOR && !throwflag) { if (co->co_flags & CO_GENERATOR && !throwflag) {
if (f->f_exc_type != NULL && f->f_exc_type != Py_None) { if (f->f_exc_type != NULL && f->f_exc_type != Py_None) {
@ -3206,6 +3207,7 @@ fast_yield:
/* pop frame */ /* pop frame */
exit_eval_frame: exit_eval_frame:
Py_LeaveRecursiveCall(); Py_LeaveRecursiveCall();
f->f_executing = 0;
tstate->frame = f->f_back; tstate->frame = f->f_back;
return retval; return retval;