8000 gh-93502: Add new C-API functions to trace object creation and destruction by pablogsal · Pull Request #115945 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-93502: Add new C-API functions to trace object creation and destruction #115945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
gh-93502: Add new C-API functions to trace object creation and
destruction
  • Loading branch information
pablogsal committed 8000 Feb 28, 2024
commit ae9c004dbd7effcae4cf39eae0df6838b61471eb
42 changes: 42 additions & 0 deletions Doc/c-api/init.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1746,6 +1746,48 @@ Python-level trace functions in previous versions.

.. versionadded:: 3.12

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to add a new section and just put the "versionadded:: 3.13" just after.

Example: "Reference Tracing".

10000
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

.. c:type:: int (*PyRefTracer)(PyObject *, int event, void* data)

The type of the trace function registered using :c:func:`PyRefTracer_SetTracer`
The first parameter is a Python object that has been just created (when **event**
is set to :c:data:`PyRefTracer_CREATE`) or about to be destroyed (when **event**
is set to :c:data:`PyRefTracer_DESTROY`). The **data** argument is the opaque pointer
that was provided when :c:func:`PyRefTracer_SetTracer` was called.

.. versionadded:: 3.13

.. c:var:: int PyRefTracer_CREATE

The value for the *event* parameter to :c:type:`PyRefTracer` functions when a Python
object has been created.

.. c:var:: int PyRefTracer_DESTROY

The value for the *event* parameter to :c:type:`PyRefTracer` functions when a Python
object has been destroyed.

.. c:function:: int PyRefTracer_SetTracer(PyRefTracer tracer, void *data)

Register a reference tracer function. The function will be called when a new Python
has been created or when an object is going to be destroyed. If **data** is provided
it must be an opaque pointer that will be provided when the tracer function is called.

Not that tracer functions **must not** create Python objects inside or otherwise the
call will be re-entrant.

The GIL must be held when calling this function.

.. versionadded:: 3.13

.. c:function:: PyRefTracer PyRefTracer_GetTracer(void** data)

Get the registered reference tracer function and the value of the opaque data pointer that
was registered when :c:func:`PyRefTracer_SetTracer` was called. If no tracer was registered
this function will return NULL and will set the **data** pointer to NULL.

The GIL must be held when calling this function.

.. versionadded:: 3.13

.. _advanced-debugging:

Expand Down
10 changes: 10 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,13 @@ PyAPI_FUNC(int) PyType_Unwatch(int watcher_id, PyObject *type);
* assigned, or 0 if a new tag could not be assigned.
*/
PyAPI_FUNC(int) PyUnstable_Type_AssignVersionTag(PyTypeObject *type);


typedef enum {
PyRefTracer_CREATE = 0,
PyRefTracer_DESTROY = 1,
} PyRefTracerEvent;

typedef int (*PyRefTracer)(PyObject *, PyRefTracerEvent event, void *);
PyAPI_FUNC(int) PyRefTracer_SetTracer(PyRefTracer tracer, void *data);
PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);
2 changes: 1 addition & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content);
when a memory block is reused from a free list.

Internal function called by _Py_NewReference(). */
extern int _PyTraceMalloc_NewReference(PyObject *op);
extern int _PyTraceMalloc_NewReference(PyObject *op, PyRefTracerEvent event, void*);

// Fast inlined version of PyType_HasFeature()
static inline int
Expand Down
7 changes: 7 additions & 0 deletions Include/internal/pycore_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ typedef struct _Py_DebugOffsets {
} tuple_object;
} _Py_DebugOffsets;

/* Reference tracer state */
struct _reftracer_runtime_state {
PyRefTracer tracer_func;
void* data;
};

/* Full Python runtime state */

/* _PyRuntimeState holds the global state for the CPython runtime.
Expand Down Expand Up @@ -229,6 +235,7 @@ typedef struct pyruntimestate {
struct _fileutils_state fileutils;
struct _faulthandler_runtime_state faulthandler;
struct _tracemalloc_runtime_state tracemalloc;
struct _reftracer_runtime_state reftracer;

// The rwmutex is used to prevent overlapping global and per-interpreter
// stop-the-world events. Global stop-the-world events lock the mutex
Expand Down
3 changes: 3 additions & 0 deletions Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ extern PyTypeObject _PyExc_MemoryError;
}, \
.faulthandler = _faulthandler_runtime_state_INIT, \
.tracemalloc = _tracemalloc_runtime_state_INIT, \
.reftracer = { \
.tracer_func = NULL, \
}, \
.stoptheworld = { \
.is_global = 1, \
}, \
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Add two new functions to the C-API, :c:func:`PyRefTracer_SetTracer` and
:c:func:`PyRefTracer_GetTracer`, that allows to track object creation and
destruction the same way the :mod:`tracemalloc` module does. Patch by Pablo
Galindo
75 changes: 75 additions & 0 deletions Modules/_testcapimodule.c
10000
Original file line number Diff line number Diff line change
Expand Up @@ -3284,6 +3284,80 @@ test_weakref_capi(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
_Py_COMP_DIAG_POP
}

struct simpletracer_data {
int create_count;
int destroy_count;
void* addresses[10];
};

static int _simpletracer(PyObject *obj, PyRefTracerEvent event, void* data) {
struct simpletracer_data* the_data = (struct simpletracer_data*)data;
assert(the_data->create_count + the_data->destroy_count < 10);
the_data->addresses[the_data->create_count + the_data->destroy_count] = obj;
if (event == PyRefTracer_CREATE) {
the_data->create_count++;
} else {
the_data->destroy_count++;
}
return 0;
}

static PyObject *
test_reftracer(PyObject *ob, PyObject *Py_UNUSED(ignored))
{
struct simpletracer_data tracer_data = {0};
void* the_data = (void*)&tracer_data;
// Install a simple tracer function
PyRefTracer_SetTracer(_simpletracer, the_data);

// Check that the tracer was correctly installed
void* data;
if (PyRefTracer_GetTracer(&data) != _simpletracer || data != the_data) {
PyErr_SetString(PyExc_ValueError, "The reftracer not correctly installed");
PyRefTracer_SetTracer(NULL, NULL);
return NULL;
}

// Create a bunch of objects
PyObject* obj = PyList_New(0);
if (obj == NULL) {
return NULL;
}
PyObject* obj2 = PyDict_New();
if (obj2 == NULL) {
Py_DECREF(obj);
return NULL;
}

// Kill all objects
Py_DECREF(obj);
Py_DECREF(obj2);

// Remove the tracer
PyRefTracer_SetTracer(NULL, NULL);

// Check that the tracer was removed
if (PyRefTracer_GetTracer(&data) != NULL || data != NULL) {
PyErr_SetString(PyExc_ValueError, "The reftracer was not correctly removed");
return NULL;
}

if (tracer_data.create_count != 2 ||
tracer_data.addresses[0] != obj ||
tracer_data.addresses[1] != obj2) {
PyErr_SetString(PyExc_ValueError, "The object creation was not correctly traced");
return NULL;
}

if (tracer_data.destroy_count != 2 ||
tracer_data.addresses[2] != obj ||
tracer_data.addresses[3] != obj2) {
PyErr_SetString(PyExc_ValueError, "The object destruction was not correctly traced");
return NULL;
}

Py_RETURN_NONE;
}

static PyMethodDef TestMethods[] = {
{"set_errno", set_errno, METH_VARARGS},
Expand Down Expand Up @@ -3320,6 +3394,7 @@ static PyMethodDef TestMethods[] = {
{"test_get_type_name", test_get_type_name, METH_NOARGS},
{"test_get_type_qualname", test_get_type_qualname, METH_NOARGS},
{"test_get_type_dict", test_get_type_dict, METH_NOARGS},
{"test_reftracer", test_reftracer, METH_NOARGS},
{"_test_thread_state", test_thread_state, METH_VARARGS},
#ifndef MS_WINDOWS
{"_spawn_pthread_waiter", spawn_pthread_waiter, METH_NOARGS},
Expand Down
35 changes: 29 additions & 6 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2367,9 +2367,6 @@ _PyTypes_FiniTypes(PyInterpreterState *interp)
static inline void
new_reference(PyObject *op)
{
if (_PyRuntime.tracemalloc.config.tracing) {
_PyTraceMalloc_NewReference(op);
}
// Skip the immortal object check in Py_SET_REFCNT; always set refcnt to 1
#if !defined(Py_GIL_DISABLED)
op->ob_refcnt = 1;
Expand All @@ -2384,6 +2381,10 @@ new_reference(PyObject *op)
#ifdef Py_TRACE_REFS
_Py_AddToAllObjects(op);
#endif
if (_PyRuntime.reftracer.tracer_func != NULL) {
void* data = _PyRuntime.reftracer.data;
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_CREATE, data);
}
}

void
Expand All @@ -2404,12 +2405,13 @@ _Py_NewReferenceNoTotal(PyObject *op)
void
_Py_ResurrectReference(PyObject *op)
{
if (_PyRuntime.tracemalloc.config.tracing) {
_PyTraceMalloc_NewReference(op);
}
#ifdef Py_TRACE_REFS
_Py_AddToAllObjects(op);
#endif
if (_PyRuntime.reftracer.tracer_func != NULL) {
void* data = _PyRuntime.reftracer.data;
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_CREATE, data);
}
}


Expand Down Expand Up @@ -2883,6 +2885,11 @@ _Py_Dealloc(PyObject *op)
Py_INCREF(type);
#endif

if (_PyRuntime.reftracer.tracer_func != NULL) {
void* data = _PyRuntime.reftracer.data;
_PyRuntime.reftracer.tracer_func(op, PyRefTracer_DESTROY, data);
}

#ifdef Py_TRACE_REFS
_Py_ForgetReference(op);
#endif
Expand Down Expand Up @@ -2970,3 +2977,19 @@ _Py_SetRefcnt(PyObject *ob, Py_ssize_t refcnt)
{
Py_SET_REFCNT(ob, refcnt);
}

int PyRefTracer_SetTracer(PyRefTracer tracer, void *data) {
assert(PyGILState_Check());
_PyRuntime.reftracer.tracer_func = tracer;
_PyRuntime.reftracer.data = data;
return 0;
}

PyRefTracer PyRefTracer_GetTracer(void** data) {
assert(PyGILState_Check());
if (data != NULL) {
*data = _PyRuntime.reftracer.data;
}
return _PyRuntime.reftracer.tracer_func;
}

9 changes: 8 additions & 1 deletion Python/tracemalloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -906,6 +906,8 @@ _PyTraceMalloc_Start(int max_nframe)
return -1;
}

_PyRuntime.reftracer.tracer_func = _PyTraceMalloc_NewReference;

if (tracemalloc_config.tracing) {
/* hook already installed: do nothing */
return 0;
Expand Down Expand Up @@ -1352,8 +1354,13 @@ _PyTraceMalloc_Fini(void)
Do nothing if tracemalloc is not tracing memory allocations
or if the object memory block is not already traced. */
int
_PyTraceMalloc_NewReference(PyObject *op)
_PyTraceMalloc_NewReference(PyObject *op, PyRefTracerEvent event, void* Py_UNUSED(ignore))

{
if (event != PyRefTracer_CREATE) {
return 0;
}

assert(PyGILState_Check());

if (!tracemalloc_config.tracing) {
Expand Down
0