Skip to content

Commit c975033

Browse files
committed
gh-93502: Add new C-API functions to trace object creation and
destruction
1 parent 7b21403 commit c975033

File tree

9 files changed

+151
-6
lines changed

9 files changed

+151
-6
lines changed

Doc/c-api/init.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,6 +1746,38 @@ Python-level trace functions in previous versions.
17461746
17471747
.. versionadded:: 3.12
17481748
1749+
.. c:type:: int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void* data)
1750+
1751+
The type of the trace function registered using :c:func:`PyRefTracer_SetTracer`
1752+
The first parameter is a Python object that has been just created (when **event**
1753+
is set to :c:data:`PyRefTracer_CREATE`) or about to be destroyed (when **event**
1754+
is set to :c:data:`PyRefTracer_CREATE`). The **data** argument is the opaque pointer
1755+
that was provided when :c:func:`PyRefTracer_SetTracer` was called.
1756+
1757+
.. versionadded:: 3.13
1758+
1759+
.. c:function:: int PyRefTracer_SetTracer(PyRefTracer tracer, void *data)
1760+
1761+
Register a reference tracer function. The function will be called when a new Python
1762+
has been created or when an object is going to be destroyed. If **data** is provided
1763+
it must be an opaque pointer that will be provided when the tracer function is called.
1764+
1765+
Not that tracer functions **must not** create Python objects inside or otherwise the
1766+
call will be re-entrant.
1767+
1768+
The GIL must be held when calling this function.
1769+
1770+
.. versionadded:: 3.13
1771+
1772+
.. c:function:: PyRefTracer PyRefTracer_GetTracer(void** data) {
1773+
1774+
Get the registered reference tracer function and the value of the opaque data pointer that
1775+
was registered when :c:func:`PyRefTracer_SetTracer` was called. If no tracer was registered
1776+
this function will return NULL and will set the **data** pointer to NULL.
1777+
1778+
The GIL must be held when calling this function.
1779+
1780+
.. versionadded:: 3.13
17491781
17501782
.. _advanced-debugging:
17511783

Include/cpython/object.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,13 @@ PyAPI_FUNC(int) PyType_Unwatch(int watcher_id, PyObject *type);
507507
* assigned, or 0 if a new tag could not be assigned.
508508
*/
509509
PyAPI_FUNC(int) PyUnstable_Type_AssignVersionTag(PyTypeObject *type);
510+
511+
512+
typedef enum {
513+
PyRefTracer_CREATE = 0,
514+
PyRefTracer_DESTROY = 1,
515+
} PyRefTracerEvent;
516+
517+
typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *);
518+
PyAPI_FUNC(int) PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
519+
PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);

Include/internal/pycore_object.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content);
252252
when a memory block is reused from a free list.
253253
254254
Internal function called by _Py_NewReference(). */
255-
extern int _PyTraceMalloc_NewReference(PyObject *op);
255+
extern int _PyTraceMalloc_NewReference(PyObject *op, PyRefTracerEvent event, void*);
256256

257257
// Fast inlined version of PyType_HasFeature()
258258
static inline int

Include/internal/pycore_runtime.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ typedef struct _Py_DebugOffsets {
125125
} tuple_object;
126126
} _Py_DebugOffsets;
127127

128+
/* Reference tracer state */
129+
struct _reftracer_runtime_state {
130+
PyRefTracer tracer_func;
131+
void* data;
132+
};
133+
128134
/* Full Python runtime state */
129135

130136
/* _PyRuntimeState holds the global state for the CPython runtime.
@@ -226,6 +232,7 @@ typedef struct pyruntimestate {
226232
struct _fileutils_state fileutils;
227233
struct _faulthandler_runtime_state faulthandler;
228234
struct _tracemalloc_runtime_state tracemalloc;
235+
struct _reftracer_runtime_state reftracer;
229236

230237
// The rwmutex is used to prevent overlapping global and per-interpreter
231238
// stop-the-world events. Global stop-the-world events lock the mutex

Include/internal/pycore_runtime_init.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ extern PyTypeObject _PyExc_MemoryError;
119119
}, \
120120
.faulthandler = _faulthandler_runtime_state_INIT, \
121121
.tracemalloc = _tracemalloc_runtime_state_INIT, \
122+
.reftracer = { \
123+
.tracer_func = NULL, \
124+
}, \
122125
.stoptheworld = { \
123126
.is_global = 1, \
124127
}, \
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add two new functions to the C-API, :c:func:`PyRefTracer_SetTracer` and
2+
:c:func:`PyRefTracer_GetTracer`, that allows to track object creation and
3+
destruction the same way the :mod:`tracemalloc` module does. Patch by Pablo
4+
Galindo

Modules/_testcapimodule.c

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,6 +3284,66 @@ test_weakref_capi(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
32843284
_Py_COMP_DIAG_POP
32853285
}
32863286

3287+
struct simpletracer_data {
3288+
int count;
3289+
void* addresses[10];
3290+
};
3291+
3292+
static int _simpletracer(PyObject *obj, PyRefTracerEvent event, void* data) {
3293+
struct simpletracer_data* the_data = (struct simpletracer_data*)data;
3294+
the_data->count++;
3295+
assert(the_data->count < 10);
3296+
the_data->addresses[the_data->count] = obj;
3297+
return 0;
3298+
}
3299+
3300+
static PyObject *
3301+
test_reftracer(PyObject *ob, PyObject *Py_UNUSED(ignored))
3302+
{
3303+
struct simpletracer_data tracer_data = {0};
3304+
void* the_data = (void*)&tracer_data;
3305+
// Install a simple tracer function
3306+
PyRefTracer_SetTracer(_simpletracer, the_data);
3307+
3308+
// Check that the tracer was correctly installed
3309+
void* data;
3310+
if (PyRefTracer_GetTracer(&data) != _simpletracer || data != the_data) {
3311+
PyErr_SetString(PyExc_ValueError, "The reftracer not correctly installed");
3312+
PyRefTracer_SetTracer(NULL, NULL);
3313+
return NULL;
3314+
}
3315+
// Create a bunch of objects
3316+
3317+
PyObject* obj = PyList_New(0);
3318+
if (obj == NULL) {
3319+
return NULL;
3320+
}
3321+
PyObject* obj2 = PyDict_New();
3322+
if (obj2 == NULL) {
3323+
Py_DECREF(obj);
3324+
return NULL;
3325+
}
3326+
3327+
// Remove the tracer
3328+
PyRefTracer_SetTracer(NULL, NULL);
3329+
3330+
Py_DECREF(obj);
3331+
Py_DECREF(obj2);
3332+
3333+
// Check that the tracer was removed
3334+
if (PyRefTracer_GetTracer(&data) != NULL || data != NULL) {
3335+
PyErr_SetString(PyExc_ValueError, "The reftracer was not correctly removed");
3336+
return NULL;
3337+
}
3338+
3339+
// Check that the objects were traced and are inside the list
3340+
if (tracer_data.count != 2 || tracer_data.addresses[1] != obj || tracer_data.addresses[2] != obj2) {
3341+
PyErr_SetString(PyExc_ValueError, "The objects were not correctly traced");
3342+
return NULL;
3343+
}
3344+
3345+
Py_RETURN_NONE;
3346+
}
32873347

32883348
static PyMethodDef TestMethods[] = {
32893349
{"set_errno", set_errno, METH_VARARGS},
@@ -3320,6 +3380,7 @@ static PyMethodDef TestMethods[] = {
33203380
{"test_get_type_name", test_get_type_name, METH_NOARGS},
33213381
{"test_get_type_qualname", test_get_type_qualname, METH_NOARGS},
33223382
{"test_get_type_dict", test_get_type_dict, METH_NOARGS},
3383+
{"test_reftracer", test_reftracer, METH_NOARGS},
33233384
{"_test_thread_state", test_thread_state, METH_VARARGS},
33243385
#ifndef MS_WINDOWS
33253386
{"_spawn_pthread_waiter", spawn_pthread_waiter, METH_NOARGS},

Objects/object.c

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,8 +2367,9 @@ _PyTypes_FiniTypes(PyInterpreterState *interp)
23672367
static inline void
23682368
new_reference(PyObject *op)
23692369
{
2370-
if (_PyRuntime.tracemalloc.config.tracing) {
2371-
_PyTraceMalloc_NewReference(op);
2370+
if (_PyRuntime.reftracer.tracer_func != NULL) {
2371+
void* data = _PyRuntime.reftracer.data;
2372+
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_CREATE, data);
23722373
}
23732374
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
23742375
#if !defined(Py_GIL_DISABLED)
@@ -2404,8 +2405,9 @@ _Py_NewReferenceNoTotal(PyObject *op)
24042405
void
24052406
_Py_ResurrectReference(PyObject *op)
24062407
{
2407-
if (_PyRuntime.tracemalloc.config.tracing) {
2408-
_PyTraceMalloc_NewReference(op);
2408+
if (_PyRuntime.reftracer.tracer_func != NULL) {
2409+
void* data = _PyRuntime.reftracer.data;
2410+
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_CREATE, data);
24092411
}
24102412
#ifdef Py_TRACE_REFS
24112413
_Py_AddToAllObjects(op);
@@ -2883,6 +2885,11 @@ _Py_Dealloc(PyObject *op)
28832885
Py_INCREF(type);
28842886
#endif
28852887

2888+
if (_PyRuntime.reftracer.tracer_func != NULL) {
2889+
void* data = _PyRuntime.reftracer.data;
2890+
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_DESTROY, data);
2891+
}
2892+
28862893
#ifdef Py_TRACE_REFS
28872894
_Py_ForgetReference(op);
28882895
#endif
@@ -2970,3 +2977,17 @@ _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt)
29702977
{
29712978
Py_SET_REFCNT(ob, refcnt);
29722979
}
2980+
2981+
int PyRefTracer_SetTracer(PyRefTracer tracer, void *data) {
2982+
_PyRuntime.reftracer.tracer_func = tracer;
2983+
_PyRuntime.reftracer.data = data;
2984+
return 0;
2985+
}
2986+
2987+
PyRefTracer PyRefTracer_GetTracer(void** data) {
2988+
if (data != NULL) {
2989+
*data = _PyRuntime.reftracer.data;
2990+
}
2991+
return _PyRuntime.reftracer.tracer_func;
2992+
}
2993+

Python/tracemalloc.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,8 @@ _PyTraceMalloc_Start(int max_nframe)
906906
return -1;
907907
}
908908

909+
_PyRuntime.reftracer.tracer_func = _PyTraceMalloc_NewReference;
910+
909911
if (tracemalloc_config.tracing) {
910912
/* hook already installed: do nothing */
911913
return 0;
@@ -1352,8 +1354,13 @@ _PyTraceMalloc_Fini(void)
13521354
Do nothing if tracemalloc is not tracing memory allocations
13531355
or if the object memory block is not already traced. */
13541356
int
1355-
_PyTraceMalloc_NewReference(PyObject *op)
1357+
_PyTraceMalloc_NewReference(PyObject *op, PyRefTracerEvent event, void* Py_UNUSED(ignore))
1358+
13561359
{
1360+
if (event != PyRefTracer_CREATE) {
1361+
return 0;
1362+
}
1363+
13571364
assert(PyGILState_Check());
13581365

13591366
if (!tracemalloc_config.tracing) {

0 commit comments

Comments
 (0)