Skip to content

Commit 6bcbee0

Browse files
authored
gh-93502: Add new C-API functions to trace object creation and destruction (#115945)
1 parent 2770d5c commit 6bcbee0

File tree

10 files changed

+207
-8
lines changed

10 files changed

+207
-8
lines changed

Doc/c-api/init.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,6 +1904,58 @@ Python-level trace functions in previous versions.
19041904
19051905
.. versionadded:: 3.12
19061906
1907+
Reference tracing
1908+
=================
1909+
1910+
.. versionadded:: 3.13
1911+
1912+
.. c:type:: int (*PyRefTracer)(PyObject *, int event, void* data)
1913+
1914+
The type of the trace function registered using :c:func:`PyRefTracer_SetTracer`.
1915+
The first parameter is a Python object that has been just created (when **event**
1916+
is set to :c:data:`PyRefTracer_CREATE`) or about to be destroyed (when **event**
1917+
is set to :c:data:`PyRefTracer_DESTROY`). The **data** argument is the opaque pointer
1918+
that was provided when :c:func:`PyRefTracer_SetTracer` was called.
1919+
1920+
.. versionadded:: 3.13
1921+
1922+
.. c:var:: int PyRefTracer_CREATE
1923+
1924+
The value for the *event* parameter to :c:type:`PyRefTracer` functions when a Python
1925+
object has been created.
1926+
1927+
.. c:var:: int PyRefTracer_DESTROY
1928+
1929+
The value for the *event* parameter to :c:type:`PyRefTracer` functions when a Python
1930+
object has been destroyed.
1931+
1932+
.. c:function:: int PyRefTracer_SetTracer(PyRefTracer tracer, void *data)
1933+
1934+
Register a reference tracer function. The function will be called when a new
1935+
Python has been created or when an object is going to be destroyed. If
1936+
**data** is provided it must be an opaque pointer that will be provided when
1937+
the tracer function is called. Return ``0`` on success. Set an exception and
1938+
return ``-1`` on error.
1939+
1940+
Not that tracer functions **must not** create Python objects inside or
1941+
otherwise the call will be re-entrant. The tracer also **must not** clear
1942+
any existing exception or set an exception. The GIL will be held every time
1943+
the tracer function is called.
1944+
1945+
The GIL must be held when calling this function.
1946+
1947+
.. versionadded:: 3.13
1948+
1949+
.. c:function:: PyRefTracer PyRefTracer_GetTracer(void** data)
1950+
1951+
Get the registered reference tracer function and the value of the opaque data
1952+
pointer that was registered when :c:func:`PyRefTracer_SetTracer` was called.
1953+
If no tracer was registered this function will return NULL and will set the
1954+
**data** pointer to NULL.
1955+
1956+
The GIL must be held when calling this function.
1957+
1958+
.. versionadded:: 3.13
19071959
19081960
.. _advanced-debugging:
19091961

Doc/whatsnew/3.13.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1961,6 +1961,11 @@ New Features
19611961
* Add :c:func:`PyType_GetModuleByDef` to the limited C API
19621962
(Contributed by Victor Stinner in :gh:`116936`.)
19631963

1964+
* Add two new functions to the C-API, :c:func:`PyRefTracer_SetTracer` and
1965+
:c:func:`PyRefTracer_GetTracer`, that allows to track object creation and
1966+
destruction the same way the :mod:`tracemalloc` module does. (Contributed
1967+
by Pablo Galindo in :gh:`93502`.)
1968+
19641969

19651970
Porting to Python 3.13
19661971
----------------------

Include/cpython/object.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,13 @@ PyAPI_FUNC(int) PyType_Unwatch(int watcher_id, PyObject *type);
510510
* assigned, or 0 if a new tag could not be assigned.
511511
*/
512512
PyAPI_FUNC(int) PyUnstable_Type_AssignVersionTag(PyTypeObject *type);
513+
514+
515+
typedef enum {
516+
PyRefTracer_CREATE = 0,
517+
PyRefTracer_DESTROY = 1,
518+
} PyRefTracerEvent;
519+
520+
typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *);
521+
PyAPI_FUNC(int) PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
522+
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
@@ -257,7 +257,7 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content);
257257
when a memory block is reused from a free list.
258258
259259
Internal function called by _Py_NewReference(). */
260-
extern int _PyTraceMalloc_NewReference(PyObject *op);
260+
extern int _PyTraceMalloc_TraceRef(PyObject *op, PyRefTracerEvent event, void*);
261261

262262
// Fast inlined version of PyType_HasFeature()
263263
static inline int

Include/internal/pycore_runtime.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ typedef struct _Py_DebugOffsets {
132132
} unicode_object;
133133
} _Py_DebugOffsets;
134134

135+
/* Reference tracer state */
136+
struct _reftracer_runtime_state {
137+
PyRefTracer tracer_func;
138+
void* tracer_data;
139+
};
140+
135141
/* Full Python runtime state */
136142

137143
/* _PyRuntimeState holds the global state for the CPython runtime.
@@ -236,6 +242,7 @@ typedef struct pyruntimestate {
236242
struct _fileutils_state fileutils;
237243
struct _faulthandler_runtime_state faulthandler;
238244
struct _tracemalloc_runtime_state tracemalloc;
245+
struct _reftracer_runtime_state ref_tracer;
239246

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

Include/internal/pycore_runtime_init.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ extern PyTypeObject _PyExc_MemoryError;
128128
}, \
129129
.faulthandler = _faulthandler_runtime_state_INIT, \
130130
.tracemalloc = _tracemalloc_runtime_state_INIT, \
131+
.ref_tracer = { \
132+
.tracer_func = NULL, \
133+
.tracer_data = NULL, \
134+
}, \
131135
.stoptheworld = { \
132136
.is_global = 1, \
133137
}, \
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: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3219,6 +3219,89 @@ test_weakref_capi(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
32193219
_Py_COMP_DIAG_POP
32203220
}
32213221

3222+
struct simpletracer_data {
3223+
int create_count;
3224+
int destroy_count;
3225+
void* addresses[10];
3226+
};
3227+
3228+
static int _simpletracer(PyObject *obj, PyRefTracerEvent event, void* data) {
3229+
struct simpletracer_data* the_data = (struct simpletracer_data*)data;
3230+
assert(the_data->create_count + the_data->destroy_count < (int)Py_ARRAY_LENGTH(the_data->addresses));
3231+
the_data->addresses[the_data->create_count + the_data->destroy_count] = obj;
3232+
if (event == PyRefTracer_CREATE) {
3233+
the_data->create_count++;
3234+
} else {
3235+
the_data->destroy_count++;
3236+
}
3237+
return 0;
3238+
}
3239+
3240+
static PyObject *
3241+
test_reftracer(PyObject *ob, PyObject *Py_UNUSED(ignored))
3242+
{
3243+
// Save the current tracer and data to restore it later
3244+
void* current_data;
3245+
PyRefTracer current_tracer = PyRefTracer_GetTracer(&current_data);
3246+
3247+
struct simpletracer_data tracer_data = {0};
3248+
void* the_data = &tracer_data;
3249+
// Install a simple tracer function
3250+
if (PyRefTracer_SetTracer(_simpletracer, the_data) != 0) {
3251+
goto failed;
3252+
}
3253+
3254+
// Check that the tracer was correctly installed
3255+
void* data;
3256+
if (PyRefTracer_GetTracer(&data) != _simpletracer || data != the_data) {
3257+
PyErr_SetString(PyExc_AssertionError, "The reftracer not correctly installed");
3258+
(void)PyRefTracer_SetTracer(NULL, NULL);
3259+
goto failed;
3260+
}
3261+
3262+
// Create a bunch of objects
3263+
PyObject* obj = PyList_New(0);
3264+
if (obj == NULL) {
3265+
goto failed;
3266+
}
3267+
PyObject* obj2 = PyDict_New();
3268+
if (obj2 == NULL) {
3269+
Py_DECREF(obj);
3270+
goto failed;
3271+
}
3272+
3273+
// Kill all objects
3274+
Py_DECREF(obj);
3275+
Py_DECREF(obj2);
3276+
3277+
// Remove the tracer
3278+
(void)PyRefTracer_SetTracer(NULL, NULL);
3279+
3280+
// Check that the tracer was removed
3281+
if (PyRefTracer_GetTracer(&data) != NULL || data != NULL) {
3282+
PyErr_SetString(PyExc_ValueError, "The reftracer was not correctly removed");
3283+
goto failed;
3284+
}
3285+
3286+
if (tracer_data.create_count != 2 ||
3287+
tracer_data.addresses[0] != obj ||
3288+
tracer_data.addresses[1] != obj2) {
3289+
PyErr_SetString(PyExc_ValueError, "The object creation was not correctly traced");
3290+
goto failed;
3291+
}
3292+
3293+
if (tracer_data.destroy_count != 2 ||
3294+
tracer_data.addresses[2] != obj ||
3295+
tracer_data.addresses[3] != obj2) {
3296+
PyErr_SetString(PyExc_ValueError, "The object destruction was not correctly traced");
3297+
goto failed;
3298+
}
3299+
PyRefTracer_SetTracer(current_tracer, current_data);
3300+
Py_RETURN_NONE;
3301+
failed:
3302+
PyRefTracer_SetTracer(current_tracer, current_data);
3303+
return NULL;
3304+
}
32223305

32233306
static PyMethodDef TestMethods[] = {
32243307
{"set_errno", set_errno, METH_VARARGS},
@@ -3257,6 +3340,7 @@ static PyMethodDef TestMethods[] = {
32573340
{"get_type_fullyqualname", get_type_fullyqualname, METH_O},
32583341
{"get_type_module_name", get_type_module_name, METH_O},
32593342
{"test_get_type_dict", test_get_type_dict, METH_NOARGS},
3343+
{"test_reftracer", test_reftracer, METH_NOARGS},
32603344
{"_test_thread_state", test_thread_state, METH_VARARGS},
32613345
#ifndef MS_WINDOWS
32623346
{"_spawn_pthread_waiter", spawn_pthread_waiter, METH_NOARGS},

Objects/object.c

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,9 +2372,6 @@ _PyTypes_FiniTypes(PyInterpreterState *interp)
23722372
static inline void
23732373
new_reference(PyObject *op)
23742374
{
2375-
if (_PyRuntime.tracemalloc.config.tracing) {
2376-
_PyTraceMalloc_NewReference(op);
2377-
}
23782375
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
23792376
#if !defined(Py_GIL_DISABLED)
23802377
op->ob_refcnt = 1;
@@ -2389,6 +2386,11 @@ new_reference(PyObject *op)
23892386
#ifdef Py_TRACE_REFS
23902387
_Py_AddToAllObjects(op);
23912388
#endif
2389+
struct _reftracer_runtime_state *tracer = &_PyRuntime.ref_tracer;
2390+
if (tracer->tracer_func != NULL) {
2391+
void* data = tracer->tracer_data;
2392+
tracer->tracer_func(op, PyRefTracer_CREATE, data);
2393+
}
23922394
}
23932395

23942396
void
@@ -2450,12 +2452,13 @@ _PyObject_SetDeferredRefcount(PyObject *op)
24502452
void
24512453
_Py_ResurrectReference(PyObject *op)
24522454
{
2453-
if (_PyRuntime.tracemalloc.config.tracing) {
2454-
_PyTraceMalloc_NewReference(op);
2455-
}
24562455
#ifdef Py_TRACE_REFS
24572456
_Py_AddToAllObjects(op);
24582457
#endif
2458+
if (_PyRuntime.ref_tracer.tracer_func != NULL) {
2459+
void* data = _PyRuntime.ref_tracer.tracer_data;
2460+
_PyRuntime.ref_tracer.tracer_func(op, PyRefTracer_CREATE, data);
2461+
}
24592462
}
24602463

24612464

@@ -2845,6 +2848,12 @@ _Py_Dealloc(PyObject *op)
28452848
Py_INCREF(type);
28462849
#endif
28472850

2851+
struct _reftracer_runtime_state *tracer = &_PyRuntime.ref_tracer;
2852+
if (tracer->tracer_func != NULL) {
2853+
void* data = tracer->tracer_data;
2854+
tracer->tracer_func(op, PyRefTracer_DESTROY, data);
2855+
}
2856+
28482857
#ifdef Py_TRACE_REFS
28492858
_Py_ForgetReference(op);
28502859
#endif
@@ -2933,6 +2942,22 @@ _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt)
29332942
Py_SET_REFCNT(ob, refcnt);
29342943
}
29352944

2945+
int PyRefTracer_SetTracer(PyRefTracer tracer, void *data) {
2946+
assert(PyGILState_Check());
2947+
_PyRuntime.ref_tracer.tracer_func = tracer;
2948+
_PyRuntime.ref_tracer.tracer_data = data;
2949+
return 0;
2950+
}
2951+
2952+
PyRefTracer PyRefTracer_GetTracer(void** data) {
2953+
assert(PyGILState_Check());
2954+
if (data != NULL) {
2955+
*data = _PyRuntime.ref_tracer.tracer_data;
2956+
}
2957+
return _PyRuntime.ref_tracer.tracer_func;
2958+
}
2959+
2960+
29362961

29372962
static PyObject* constants[] = {
29382963
&_Py_NoneStruct, // Py_CONSTANT_NONE

Python/tracemalloc.c

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

909+
if (PyRefTracer_SetTracer(_PyTraceMalloc_TraceRef, NULL) < 0) {
910+
return -1;
911+
}
912+
909913
if (tracemalloc_config.tracing) {
910914
/* hook already installed: do nothing */
911915
return 0;
@@ -1352,8 +1356,12 @@ _PyTraceMalloc_Fini(void)
13521356
Do nothing if tracemalloc is not tracing memory allocations
13531357
or if the object memory block is not already traced. */
13541358
int
1355-
_PyTraceMalloc_NewReference(PyObject *op)
1359+
_PyTraceMalloc_TraceRef(PyObject *op, PyRefTracerEvent event, void* Py_UNUSED(ignore))
13561360
{
1361+
if (event != PyRefTracer_CREATE) {
1362+
return 0;
1363+
}
1364+
13571365
assert(PyGILState_Check());
13581366

13591367
if (!tracemalloc_config.tracing) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy