The new `_Py_DebugOffsets` structure should include structure sizes

I started looking into adding support for CPython 3.13 to Austin. The new _Py_DebugOffsets structure at the beginning of _PyRuntime has been introduced to make out-of-process tools, such as Austin, easier to maintain going forward. However, one thing that I would find useful if it was included in the new debug structure is an extra field carrying the size of each structure. Whilst not strictly necessary, having this fields would allow for a reduced number of system calls to read the remote memory of the attached process. That’s because, knowing the size of a structure, one can make a local copy of it, and then use the offsets to resolve the fields from the local copy. With only the offsets available, one would have to make a system call to read the remote memory for each field of the structure that needs to be resolved.

I understand we are past the first beta release, but considering this is not something that’s part of the public API, I would suggest backporting the change to 3.13, if approved.

diff --git a/Include/internal/pycore_runtime.h b/Include/internal/pycore_runtime.h
index f58eccf729c..341fe29a68a 100644
--- a/Include/internal/pycore_runtime.h
+++ b/Include/internal/pycore_runtime.h
@@ -55,12 +55,14 @@ typedef struct _Py_DebugOffsets {
     uint64_t version;
     // Runtime state offset;
     struct _runtime_state {
+        uint64_t size;
         uint64_t finalizing;
         uint64_t interpreters_head;
     } runtime_state;
 
     // Interpreter state offset;
     struct _interpreter_state {
+        uint64_t size;
         uint64_t next;
         uint64_t threads_head;
         uint64_t gc;
@@ -74,6 +76,7 @@ typedef struct _Py_DebugOffsets {
 
     // Thread state offset;
     struct _thread_state{
+        uint64_t size;
         uint64_t prev;
         uint64_t next;
         uint64_t interp;
@@ -84,6 +87,7 @@ typedef struct _Py_DebugOffsets {
 
     // InterpreterFrame offset;
     struct _interpreter_frame {
+        uint64_t size;
         uint64_t previous;
         uint64_t executable;
         uint64_t instr_ptr;
@@ -93,12 +97,14 @@ typedef struct _Py_DebugOffsets {
 
     // CFrame offset;
     struct _cframe {
+        uint64_t size;
         uint64_t current_frame;
         uint64_t previous;
     } cframe;
 
     // Code object offset;
     struct _code_object {
+        uint64_t size;
         uint64_t filename;
         uint64_t name;
         uint64_t linetable;
@@ -111,21 +117,25 @@ typedef struct _Py_DebugOffsets {
 
     // PyObject offset;
     struct _pyobject {
+        uint64_t size;
         uint64_t ob_type;
     } pyobject;
 
     // PyTypeObject object offset;
     struct _type_object {
+        uint64_t size;
         uint64_t tp_name;
     } type_object;
 
     // PyTuple object offset;
     struct _tuple_object {
+        uint64_t size;
         uint64_t ob_item;
     } tuple_object;
 
     // Unicode object offset;
     struct _unicode_object {
+        uint64_t size;
         uint64_t state;
         uint64_t length;
         size_t asciiobject_size;
diff --git a/Include/internal/pycore_runtime_init.h b/Include/internal/pycore_runtime_init.h
index 98920dbb7c7..33e39c2edbe 100644
--- a/Include/internal/pycore_runtime_init.h
+++ b/Include/internal/pycore_runtime_init.h
@@ -35,10 +35,12 @@ extern PyTypeObject _PyExc_MemoryError;
             .cookie = "xdebugpy", \
             .version = PY_VERSION_HEX, \
             .runtime_state = { \
+                .size = sizeof(_PyRuntimeState), \
                 .finalizing = offsetof(_PyRuntimeState, _finalizing), \
                 .interpreters_head = offsetof(_PyRuntimeState, interpreters.head), \
             }, \
             .interpreter_state = { \
+                .size = sizeof(PyInterpreterState), \
                 .next = offsetof(PyInterpreterState, next), \
                 .threads_head = offsetof(PyInterpreterState, threads.head), \
                 .gc = offsetof(PyInterpreterState, gc), \
@@ -50,6 +52,7 @@ extern PyTypeObject _PyExc_MemoryError;
                 .gil_runtime_state_holder = offsetof(PyInterpreterState, _gil.last_holder), \
             }, \
             .thread_state = { \
+                .size = sizeof(PyThreadState), \
                 .prev = offsetof(PyThreadState, prev), \
                 .next = offsetof(PyThreadState, next), \
                 .interp = offsetof(PyThreadState, interp), \
@@ -58,6 +61,7 @@ extern PyTypeObject _PyExc_MemoryError;
                 .native_thread_id = offsetof(PyThreadState, native_thread_id), \
             }, \
             .interpreter_frame = { \
+                .size = sizeof(_PyInterpreterFrame), \
                 .previous = offsetof(_PyInterpreterFrame, previous), \
                 .executable = offsetof(_PyInterpreterFrame, f_executable), \
                 .instr_ptr = offsetof(_PyInterpreterFrame, instr_ptr), \
@@ -65,6 +69,7 @@ extern PyTypeObject _PyExc_MemoryError;
                 .owner = offsetof(_PyInterpreterFrame, owner), \
             }, \
             .code_object = { \
+                .size = sizeof(PyCodeObject), \
                 .filename = offsetof(PyCodeObject, co_filename), \
                 .name = offsetof(PyCodeObject, co_name), \
                 .linetable = offsetof(PyCodeObject, co_linetable), \
@@ -75,15 +80,19 @@ extern PyTypeObject _PyExc_MemoryError;
                 .co_code_adaptive = offsetof(PyCodeObject, co_code_adaptive), \
             }, \
             .pyobject = { \
+                .size = sizeof(PyObject), \
                 .ob_type = offsetof(PyObject, ob_type), \
             }, \
             .type_object = { \
+                .size = sizeof(PyTypeObject), \
                 .tp_name = offsetof(PyTypeObject, tp_name), \
             }, \
             .tuple_object = { \
+                .size = sizeof(PyTupleObject), \
                 .ob_item = offsetof(PyTupleObject, ob_item), \
             }, \
             .unicode_object = { \
+                .size = sizeof(PyUnicodeObject), \
                 .state = offsetof(PyUnicodeObject, _base._base.state), \
                 .length = offsetof(PyUnicodeObject, _base._base.length), \
                 .asciiobject_size = sizeof(PyASCIIObject), \
1 Like

I suggest you proposing a PR rather than sending a patch here.

Sure, I just wanted to gather some feedback before opening an issue and a PR for this, in case there was some major pushback.

1 Like

PR Add structure sizes to the _Py_DebugOffsets structure by P403n1x87 · Pull Request #121230 · python/cpython · GitHub