Skip to content

Commit a7a7653

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

File tree

9 files changed

+169
-8
lines changed

9 files changed

+169
-8
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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3284,6 +3284,80 @@ test_weakref_capi(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
32843284
_Py_COMP_DIAG_POP
32853285
}
32863286

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

32883362
static PyMethodDef TestMethods[] = {
32893363
{"set_errno", set_errno, METH_VARARGS},
@@ -3320,6 +3394,7 @@ static PyMethodDef TestMethods[] = {
33203394
{"test_get_type_name", test_get_type_name, METH_NOARGS},
33213395
{"test_get_type_qualname", test_get_type_qualname, METH_NOARGS},
33223396
{"test_get_type_dict", test_get_type_dict, METH_NOARGS},
3397+
{"test_reftracer", test_reftracer, METH_NOARGS},
33233398
{"_test_thread_state", test_thread_state, METH_VARARGS},
33243399
#ifndef MS_WINDOWS
33253400
{"_spawn_pthread_waiter", spawn_pthread_waiter, METH_NOARGS},

Objects/object.c

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,9 +2367,6 @@ _PyTypes_FiniTypes(PyInterpreterState *interp)
23672367
static inline void
23682368
new_reference(PyObject *op)
23692369
{
2370-
if (_PyRuntime.tracemalloc.config.tracing) {
2371-
_PyTraceMalloc_NewReference(op);
2372-
}
23732370
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
23742371
#if !defined(Py_GIL_DISABLED)
23752372
op->ob_refcnt = 1;
@@ -2384,6 +2381,10 @@ new_reference(PyObject *op)
23842381
#ifdef Py_TRACE_REFS
23852382
_Py_AddToAllObjects(op);
23862383
#endif
2384+
if (_PyRuntime.reftracer.tracer_func != NULL) {
2385+
void* data = _PyRuntime.reftracer.data;
2386+
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_CREATE, data);
2387+
}
23872388
}
23882389

23892390
void
@@ -2404,12 +2405,13 @@ _Py_NewReferenceNoTotal(PyObject *op)
24042405
void
24052406
_Py_ResurrectReference(PyObject *op)
24062407
{
2407-
if (_PyRuntime.tracemalloc.config.tracing) {
2408-
_PyTraceMalloc_NewReference(op);
2409-
}
24102408
#ifdef Py_TRACE_REFS
24112409
_Py_AddToAllObjects(op);
24122410
#endif
2411+
if (_PyRuntime.reftracer.tracer_func != NULL) {
2412+
void* data = _PyRuntime.reftracer.data;
2413+
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_CREATE, data);
2414+
}
24132415
}
24142416

24152417

@@ -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,19 @@ _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+
assert(PyGILState_Check());
2983+
_PyRuntime.reftracer.tracer_func = tracer;
2984+
_PyRuntime.reftracer.data = data;
2985+
return 0;
2986+
}
2987+
2988+
PyRefTracer PyRefTracer_GetTracer(void** data) {
2989+
assert(PyGILState_Check());
2990+
if (data != NULL) {
2991+
*data = _PyRuntime.reftracer.data;
2992+
}
2993+
return _PyRuntime.reftracer.tracer_func;
2994+
}
2995+

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)