Skip to content

Commit f85d9b7

Browse files
committed
fix: prevent code objects from leaking #1924
1 parent ae8d3b9 commit f85d9b7

File tree

3 files changed

+42
-11
lines changed

3 files changed

+42
-11
lines changed

Diff for: CHANGES.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,16 @@ upgrading your version of coverage.py.
2323
Unreleased
2424
----------
2525

26+
- Fix: a memory leak in CTracer has been fixed. The details are in `issue
27+
1924`_ and `pytest-dev 676`_. This should reduce the memory footprint for
28+
everyone even if it hadn't caused a problem before.
29+
2630
- We now ship a py3-none-any.whl wheel file. Thanks, `Russell Keith-Magee
2731
<pull 1914_>`_.
2832

2933
.. _pull 1914: https://github.com/nedbat/coveragepy/pull/1914
30-
34+
.. _issue 1924: https://github.com/nedbat/coveragepy/issues/1924
35+
.. _pytest-dev 676: https://github.com/pytest-dev/pytest-cov/issues/676
3136

3237
.. start-releases
3338

Diff for: coverage/ctracer/tracer.c

+21-10
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,17 @@ CTracer_set_pdata_stack(CTracer *self)
288288
return ret;
289289
}
290290

291+
// Thanks for the idea, memray!
292+
inline PyCodeObject*
293+
MyFrame_BorrowCode(PyFrameObject* frame)
294+
{
295+
// Return a borrowed reference.
296+
PyCodeObject* pCode = PyFrame_GetCode(frame);
297+
assert(Py_REFCNT(pCode) >= 2);
298+
Py_DECREF(pCode);
299+
return pCode;
300+
}
301+
291302
/*
292303
* Parts of the trace function.
293304
*/
@@ -359,7 +370,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame)
359370
}
360371

361372
/* Check if we should trace this line. */
362-
filename = PyFrame_GetCode(frame)->co_filename;
373+
filename = MyFrame_BorrowCode(frame)->co_filename;
363374
disposition = PyDict_GetItem(self->should_trace_cache, filename);
364375
if (disposition == NULL) {
365376
if (PyErr_Occurred()) {
@@ -554,15 +565,15 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame)
554565
* The current opcode is guaranteed to be RESUME. The argument
555566
* determines what kind of resume it is.
556567
*/
557-
pCode = MyCode_GetCode(PyFrame_GetCode(frame));
568+
pCode = MyCode_GetCode(MyFrame_BorrowCode(frame));
558569
real_call = (PyBytes_AS_STRING(pCode)[MyFrame_GetLasti(frame) + 1] == 0);
559570
#else
560571
// f_lasti is -1 for a true call, and a real byte offset for a generator re-entry.
561572
real_call = (MyFrame_GetLasti(frame) < 0);
562573
#endif
563574

564575
if (real_call) {
565-
self->pcur_entry->last_line = -PyFrame_GetCode(frame)->co_firstlineno;
576+
self->pcur_entry->last_line = -MyFrame_BorrowCode(frame)->co_firstlineno;
566577
}
567578
else {
568579
self->pcur_entry->last_line = PyFrame_GetLineNumber(frame);
@@ -649,7 +660,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame)
649660

650661
STATS( self->stats.lines++; )
651662
if (self->pdata_stack->depth >= 0) {
652-
SHOWLOG(PyFrame_GetLineNumber(frame), PyFrame_GetCode(frame)->co_filename, "line");
663+
SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_BorrowCode(frame)->co_filename, "line");
653664
if (self->pcur_entry->file_data) {
654665
int lineno_from = -1;
655666
int lineno_to = -1;
@@ -727,7 +738,7 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame)
727738
self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth];
728739
if (self->tracing_arcs && self->pcur_entry->file_data) {
729740
BOOL real_return = FALSE;
730-
pCode = MyCode_GetCode(PyFrame_GetCode(frame));
741+
pCode = MyCode_GetCode(MyFrame_BorrowCode(frame));
731742
int lasti = MyFrame_GetLasti(frame);
732743
Py_ssize_t code_size = PyBytes_GET_SIZE(pCode);
733744
unsigned char * code_bytes = (unsigned char *)PyBytes_AS_STRING(pCode);
@@ -759,7 +770,7 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame)
759770
real_return = !(is_yield || is_yield_from);
760771
#endif
761772
if (real_return) {
762-
int first = PyFrame_GetCode(frame)->co_firstlineno;
773+
int first = MyFrame_BorrowCode(frame)->co_firstlineno;
763774
if (CTracer_record_pair(self, self->pcur_entry->last_line, -first) < 0) {
764775
goto error;
765776
}
@@ -782,7 +793,7 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame)
782793
}
783794

784795
/* Pop the stack. */
785-
SHOWLOG(PyFrame_GetLineNumber(frame), PyFrame_GetCode(frame)->co_filename, "return");
796+
SHOWLOG(PyFrame_GetLineNumber(frame), MyFrame_BorrowCode(frame)->co_filename, "return");
786797
self->pdata_stack->depth--;
787798
self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth];
788799
}
@@ -824,13 +835,13 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
824835
if (what <= (int)(sizeof(what_sym)/sizeof(const char *))) {
825836
w = what_sym[what];
826837
}
827-
ascii = PyUnicode_AsASCIIString(PyFrame_GetCode(frame)->co_filename);
838+
ascii = PyUnicode_AsASCIIString(MyFrame_BorrowCode(frame)->co_filename);
828839
printf("%x trace: f:%x %s @ %s %d\n", (int)self, (int)frame, what_sym[what], PyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame));
829840
Py_DECREF(ascii);
830841
#endif
831842

832843
#if TRACE_LOG
833-
ascii = PyUnicode_AsASCIIString(PyFrame_GetCode(frame)->co_filename);
844+
ascii = PyUnicode_AsASCIIString(MyFrame_BorrowCode(frame)->co_filename);
834845
if (strstr(PyBytes_AS_STRING(ascii), start_file) && PyFrame_GetLineNumber(frame) == start_line) {
835846
logging = TRUE;
836847
}
@@ -926,7 +937,7 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds)
926937
}
927938

928939
#if WHAT_LOG
929-
ascii = PyUnicode_AsASCIIString(PyFrame_GetCode(frame)->co_filename);
940+
ascii = PyUnicode_AsASCIIString(MyFrame_BorrowCode(frame)->co_filename);
930941
printf("pytrace: %s @ %s %d\n", what_sym[what], PyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame));
931942
Py_DECREF(ascii);
932943
#endif

Diff for: tests/test_oddball.py

+15
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,21 @@ def once(x): # line 301
206206
if fails > 8:
207207
pytest.fail("RAM grew by %d" % (ram_growth)) # pragma: only failure
208208

209+
@pytest.mark.skipif(not testenv.C_TRACER, reason="Only the C tracer has refcounting issues")
210+
# In fact, sysmon explicitly holds onto all code objects,
211+
# so this will definitely fail with sysmon.
212+
def test_eval_codeobject_leak(self) -> None:
213+
# https://github.com/nedbat/coveragepy/issues/1924
214+
code = """\
215+
for i in range(100_000):
216+
r = eval("'a' + '1'")
217+
assert r == 'a1'
218+
"""
219+
ram_0 = osinfo.process_ram()
220+
self.check_coverage(code, [1, 2, 3], "")
221+
ram_growth = osinfo.process_ram() - ram_0
222+
assert ram_growth < 2_000 * 1024
223+
209224

210225
class MemoryFumblingTest(CoverageTest):
211226
"""Test that we properly manage the None refcount."""

0 commit comments

Comments
 (0)