diff --git a/Doc/library/tracemalloc.rst b/Doc/library/tracemalloc.rst index 2370d927292eb0..ee75cd1e10a003 100644 --- a/Doc/library/tracemalloc.rst +++ b/Doc/library/tracemalloc.rst @@ -299,7 +299,8 @@ Functions .. function:: clear_traces() - Clear traces of memory blocks allocated by Python. + Clear traces of memory blocks allocated and counts of created and destroyed + memory blocks by Python. See also :func:`stop`. @@ -330,6 +331,14 @@ Functions :mod:`tracemalloc` module as a tuple: ``(current: int, peak: int)``. +.. function:: get_traced_refs() + + Get the current count of created and destroyed traced references. + :mod:`tracemalloc` module as a tuple: ``(created: int, destroyed: int)``. + + .. versionadded:: next + + .. function:: reset_peak() Set the peak size of memory blocks traced by the :mod:`tracemalloc` module diff --git a/Include/internal/pycore_tracemalloc.h b/Include/internal/pycore_tracemalloc.h index 572e8025876319..b3fe287d48e176 100644 --- a/Include/internal/pycore_tracemalloc.h +++ b/Include/internal/pycore_tracemalloc.h @@ -94,6 +94,12 @@ struct _tracemalloc_runtime_state { /* domain (unsigned int) => traces (_Py_hashtable_t). Protected by TABLES_LOCK(). */ _Py_hashtable_t *domains; + /* Number of references created. + Protected by TABLES_LOCK(). */ + Py_ssize_t refs_created; + /* Number of references destroyed. + Protected by TABLES_LOCK(). */ + Py_ssize_t refs_destroyed; struct tracemalloc_traceback empty_traceback; @@ -155,6 +161,9 @@ extern size_t _PyTraceMalloc_GetMemory(void); /* Get the current size and peak size of traced memory blocks as a 2-tuple */ extern PyObject* _PyTraceMalloc_GetTracedMemory(void); +/* Get the current number of references created and destroyed as a 2-tuple */ +extern PyObject* _PyTraceMalloc_GetTracedRefs(void); + /* Set the peak size of traced memory blocks to the current size */ extern void _PyTraceMalloc_ResetPeak(void); diff --git a/Lib/test/test_tracemalloc.py b/Lib/test/test_tracemalloc.py index 0220a83d24b428..359682238e3e75 100644 --- a/Lib/test/test_tracemalloc.py +++ b/Lib/test/test_tracemalloc.py @@ -4,6 +4,7 @@ import textwrap import tracemalloc import unittest +import warnings from unittest.mock import patch from test.support.script_helper import (assert_python_ok, assert_python_failure, interpreter_requires_environment) @@ -1141,6 +1142,37 @@ def __del__(self, untrack=_testcapi.tracemalloc_untrack): """) assert_python_ok("-c", code) + def test_trace_refs(self): + def f(): + l = [] + del l + + def g(): + l = [], [] + del l + + tracemalloc.start() + + try: + tracemalloc.clear_traces() + f() + refs = tracemalloc.get_traced_refs() + if refs == (1, 0): + warnings.warn("ceval Py_DECREF doesn't emit PyRefTracer_DESTROY in this build") + else: + self.assertEqual(refs, (1, 1)) + + tracemalloc.clear_traces() + g() + refs = tracemalloc.get_traced_refs() + if refs == (3, 2): + warnings.warn("ceval Py_DECREF doesn't emit PyRefTracer_DESTROY in this build") + else: + self.assertEqual(refs, (3, 3)) + + finally: + tracemalloc.stop() + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-28-17-13-36.gh-issue-130695.WuCmBj.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-28-17-13-36.gh-issue-130695.WuCmBj.rst new file mode 100644 index 00000000000000..94ae45c6b25c40 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-28-17-13-36.gh-issue-130695.WuCmBj.rst @@ -0,0 +1 @@ +Add tracking of number of deallocations to :mod:`tracemalloc`. diff --git a/Modules/_tracemalloc.c b/Modules/_tracemalloc.c index be71fc9fc9c341..876c2e5fe5a8c7 100644 --- a/Modules/_tracemalloc.c +++ b/Modules/_tracemalloc.c @@ -167,6 +167,23 @@ _tracemalloc_get_traced_memory_impl(PyObject *module) return _PyTraceMalloc_GetTracedMemory(); } + +/*[clinic input] +_tracemalloc.get_traced_refs + +Get the current count of created and destroyed refs. + +Returns a tuple: (created: int, destroyed: int). +[clinic start generated code]*/ + +static PyObject * +_tracemalloc_get_traced_refs_impl(PyObject *module) +/*[clinic end generated code: output=81d36fdeb3ffc362 input=d0652f2592733b0e]*/ +{ + return _PyTraceMalloc_GetTracedRefs(); +} + + /*[clinic input] _tracemalloc.reset_peak @@ -195,6 +212,7 @@ static PyMethodDef module_methods[] = { _TRACEMALLOC_GET_TRACEBACK_LIMIT_METHODDEF _TRACEMALLOC_GET_TRACEMALLOC_MEMORY_METHODDEF _TRACEMALLOC_GET_TRACED_MEMORY_METHODDEF + _TRACEMALLOC_GET_TRACED_REFS_METHODDEF _TRACEMALLOC_RESET_PEAK_METHODDEF /* sentinel */ {NULL, NULL} diff --git a/Modules/clinic/_tracemalloc.c.h b/Modules/clinic/_tracemalloc.c.h index 1d100247423991..ac408cf6c51e38 100644 --- a/Modules/clinic/_tracemalloc.c.h +++ b/Modules/clinic/_tracemalloc.c.h @@ -195,6 +195,26 @@ _tracemalloc_get_traced_memory(PyObject *module, PyObject *Py_UNUSED(ignored)) return _tracemalloc_get_traced_memory_impl(module); } +PyDoc_STRVAR(_tracemalloc_get_traced_refs__doc__, +"get_traced_refs($module, /)\n" +"--\n" +"\n" +"Get the current count of created and destroyed refs.\n" +"\n" +"Returns a tuple: (created: int, destroyed: int)."); + +#define _TRACEMALLOC_GET_TRACED_REFS_METHODDEF \ + {"get_traced_refs", (PyCFunction)_tracemalloc_get_traced_refs, METH_NOARGS, _tracemalloc_get_traced_refs__doc__}, + +static PyObject * +_tracemalloc_get_traced_refs_impl(PyObject *module); + +static PyObject * +_tracemalloc_get_traced_refs(PyObject *module, PyObject *Py_UNUSED(ignored)) +{ + return _tracemalloc_get_traced_refs_impl(module); +} + PyDoc_STRVAR(_tracemalloc_reset_peak__doc__, "reset_peak($module, /)\n" "--\n" @@ -214,4 +234,4 @@ _tracemalloc_reset_peak(PyObject *module, PyObject *Py_UNUSED(ignored)) { return _tracemalloc_reset_peak_impl(module); } -/*[clinic end generated code: output=9d4d884b156c2ddb input=a9049054013a1b77]*/ +/*[clinic end generated code: output=7efbb9b4f08daab3 input=a9049054013a1b77]*/ diff --git a/Python/tracemalloc.c b/Python/tracemalloc.c index d69b0ebd585a7f..90636d7f1e19a8 100644 --- a/Python/tracemalloc.c +++ b/Python/tracemalloc.c @@ -76,6 +76,8 @@ typedef struct { #define tracemalloc_tracebacks _PyRuntime.tracemalloc.tracebacks #define tracemalloc_traces _PyRuntime.tracemalloc.traces #define tracemalloc_domains _PyRuntime.tracemalloc.domains +#define tracemalloc_refs_created _PyRuntime.tracemalloc.refs_created +#define tracemalloc_refs_destroyed _PyRuntime.tracemalloc.refs_destroyed #ifdef TRACE_DEBUG @@ -1253,42 +1255,47 @@ _PyTraceMalloc_Fini(void) Do nothing if tracemalloc is not tracing memory allocations or if the object memory block is not already traced. */ -static int -_PyTraceMalloc_TraceRef(PyObject *op, PyRefTracerEvent event, - void* Py_UNUSED(ignore)) -{ - if (event != PyRefTracer_CREATE) { - return 0; - } - if (get_reentrant()) { - return 0; - } - - _Py_AssertHoldsTstate(); - TABLES_LOCK(); - - if (!tracemalloc_config.tracing) { - goto done; - } - - PyTypeObject *type = Py_TYPE(op); - const size_t presize = _PyType_PreHeaderSize(type); - uintptr_t ptr = (uintptr_t)((char *)op - presize); - - trace_t *trace = _Py_hashtable_get(tracemalloc_traces, TO_PTR(ptr)); - if (trace != NULL) { - /* update the traceback of the memory block */ - traceback_t *traceback = traceback_new(); - if (traceback != NULL) { - trace->traceback = traceback; - } - } - /* else: cannot track the object, its memory block size is unknown */ - -done: - TABLES_UNLOCK(); - return 0; -} + static int + _PyTraceMalloc_TraceRef(PyObject *op, PyRefTracerEvent event, + void* Py_UNUSED(ignore)) + { + if (get_reentrant()) { + return 0; + } + + _Py_AssertHoldsTstate(); + TABLES_LOCK(); + + if (!tracemalloc_config.tracing) { + goto done; + } + + if (event == PyRefTracer_CREATE) { + tracemalloc_refs_created += 1; + } + else { + tracemalloc_refs_destroyed += 1; + goto done; + } + + PyTypeObject *type = Py_TYPE(op); + const size_t presize = _PyType_PreHeaderSize(type); + uintptr_t ptr = (uintptr_t)((char *)op - presize); + + trace_t *trace = _Py_hashtable_get(tracemalloc_traces, TO_PTR(ptr)); + if (trace != NULL) { + /* update the traceback of the memory block */ + traceback_t *traceback = traceback_new(); + if (traceback != NULL) { + trace->traceback = traceback; + } + } + /* else: cannot track the object, its memory block size is unknown */ + + done: + TABLES_UNLOCK(); + return 0; + } PyObject* @@ -1326,6 +1333,8 @@ _PyTraceMalloc_ClearTraces(void) TABLES_LOCK(); if (tracemalloc_config.tracing) { tracemalloc_clear_traces_unlocked(); + tracemalloc_refs_created = 0; + tracemalloc_refs_destroyed = 0; } TABLES_UNLOCK(); } @@ -1464,6 +1473,25 @@ _PyTraceMalloc_GetTracedMemory(void) return Py_BuildValue("nn", traced, peak); } + +PyObject * +_PyTraceMalloc_GetTracedRefs(void) +{ + TABLES_LOCK(); + Py_ssize_t created, destroyed; + if (tracemalloc_config.tracing) { + created = tracemalloc_refs_created; + destroyed = tracemalloc_refs_destroyed; + } + else { + created = 0; + destroyed = 0; + } + TABLES_UNLOCK(); + + return Py_BuildValue("nn", created, destroyed); +} + void _PyTraceMalloc_ResetPeak(void) {