Repository: vmprof/vmprof-python (language: python)

Issue:
crashes if the script is extremely short-running
If x.py just contains "print 42", then "python -m vmprof x.py" will likely crash because the list recorded by the profiler is empty.



Discussion / hints:
fixed


Fix patch:
diff --git a/setup.py b/setup.py
index f12854f7..8232b0f2 100644
--- a/setup.py
+++ b/setup.py
@@ -78,6 +78,8 @@ def run(self):
         raise NotImplementedError("platform '%s' is not supported!" % sys.platform)
     extra_compile_args.append('-I src/')
     extra_compile_args.append('-I src/libbacktrace')
+    if sys.version_info[:2] == (3,11):
+        extra_source_files += ['src/populate_frames.c']
     ext_modules = [Extension('_vmprof',
                            sources=[
                                'src/_vmprof.c',
@@ -121,7 +123,7 @@ def run(self):
         'pytz',
         'colorama',
     ] + extra_install_requires,
-    python_requires='>=3.6, <3.11',
+    python_requires='>=3.6, <3.12',
     tests_require=['pytest','cffi','hypothesis'],
     entry_points = {
         'console_scripts': [
diff --git a/src/_vmprof.c b/src/_vmprof.c
index cca6b991..26a7591f 100644
--- a/src/_vmprof.c
+++ b/src/_vmprof.c
@@ -11,6 +11,12 @@
 #include "_vmprof.h"
 #include "vmprof_common.h"
 
+
+#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
+#include "internal/pycore_frame.h"
+#include "populate_frames.h"
+#endif
+
 static destructor Original_code_dealloc = 0;
 static PyObject* (*_default_eval_loop)(PyFrameObject *, int) = 0;
 
@@ -112,7 +118,7 @@ static int _look_for_code_object(PyObject *o, void * param)
         /* as a special case, recursively look for and add code
            objects found in the co_consts.  The problem is that code
            objects are not created as GC-aware in CPython, so we need
-           to hack like this to hope to find most of them. 
+           to hack like this to hope to find most of them.
         */
         i = PyTuple_Size(co->co_consts);
         while (i > 0) {
@@ -267,6 +273,15 @@ write_all_code_objects(PyObject *module, PyObject * seen_code_ids)
 }
 
 
+#if PY_VERSION_HEX < 0x030900B1 /* < 3.9 */
+static inline PyFrameObject* PyThreadState_GetFrame(PyThreadState *tstate)
+{
+    Py_XINCREF(tstate->frame);
+    return tstate->frame;
+}
+#endif
+
+
 
 static PyObject *
 sample_stack_now(PyObject *module, PyObject * args)
@@ -298,7 +313,17 @@ sample_stack_now(PyObject *module, PyObject * args)
         vmprof_ignore_signals(0);
         return NULL;
     }
-    entry_count = vmp_walk_and_record_stack(tstate->frame, m, SINGLE_BUF_SIZE/sizeof(void*)-1, (int)skip, 0);
+
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11, no pypy 3.11 at the moment*/ 
+    _PyInterpreterFrame *  frame = unsafe_PyThreadState_GetInterpreterFrame(tstate);
+#else
+    PyFrameObject* frame = PyThreadState_GetFrame(tstate);
+#endif
+
+    entry_count = vmp_walk_and_record_stack(frame, m, SINGLE_BUF_SIZE/sizeof(void*)-1, (int)skip, 0);
+
+    Py_XDECREF(frame);
+
 
     for (i = 0; i < entry_count; i++) {
         routine_ip = m[i];
diff --git a/src/populate_frames.c b/src/populate_frames.c
new file mode 100644
index 00000000..e9d2bfac
--- /dev/null
+++ b/src/populate_frames.c
@@ -0,0 +1,76 @@
+/* This code was taken from https://github.com/GoogleCloudPlatform/cloud-profiler-python/blob/main/googlecloudprofiler/src/populate_frames.cc */ 
+
+#include "populate_frames.h"
+
+#include <Python.h>
+
+
+// 0x030B0000 is 3.11.
+#define PY_311 0x030B0000
+#if PY_VERSION_HEX >= PY_311
+
+/**
+ * The PyFrameObject structure members have been removed from the public C API
+ * in 3.11:
+https://docs.python.org/3/whatsnew/3.11.html#pyframeobject-3-11-hiding.
+ *
+ * Instead, getters are provided which participate in reference counting; since
+ * this code runs as part of the SIGPROF handler, it cannot modify Python
+ * objects (including their refcounts) and the getters can't be used. Instead,
+ * we expose the internal _PyInterpreterFrame and use that directly.
+ *
+ */
+
+#define Py_BUILD_CORE
+#include "internal/pycore_frame.h"
+#undef Py_BUILD_CORE
+
+// Modified from
+// https://github.com/python/cpython/blob/v3.11.4/Python/pystate.c#L1278-L1285
+_PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame(
+    PyThreadState *tstate) {
+  assert(tstate != NULL);
+  _PyInterpreterFrame *f = tstate->cframe->current_frame;
+  while (f && _PyFrame_IsIncomplete(f)) {
+    f = f->previous;
+  }
+  if (f == NULL) {
+    return NULL;
+  }
+  return f;
+}
+
+// Modified from
+// https://github.com/python/cpython/blob/v3.11.4/Objects/frameobject.c#L1310-L1315
+// with refcounting removed
+PyCodeObject *unsafe_PyInterpreterFrame_GetCode(
+    _PyInterpreterFrame *frame) {
+  assert(frame != NULL);
+  assert(!_PyFrame_IsIncomplete(frame));
+  PyCodeObject *code = frame->f_code;
+  assert(code != NULL);
+  return code;
+}
+
+// Modified from
+// https://github.com/python/cpython/blob/v3.11.4/Objects/frameobject.c#L1326-L1329
+// with refcounting removed
+_PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack(
+    _PyInterpreterFrame *frame) {
+  assert(frame != NULL);
+  assert(!_PyFrame_IsIncomplete(frame));
+  _PyInterpreterFrame *prev = frame->previous;
+  while (prev && _PyFrame_IsIncomplete(prev)) {
+    prev = prev->previous;
+  }
+  return prev;
+}
+
+// Copied from
+// https://github.com/python/cpython/blob/v3.11.4/Python/frame.c#L165-L170 as
+// this function is not available in libpython
+int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame) {
+  int addr = _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT);
+  return PyCode_Addr2Line(frame->f_code, addr);
+}
+#endif  // PY_VERSION_HEX >= PY_311
\ No newline at end of file
diff --git a/src/populate_frames.h b/src/populate_frames.h
new file mode 100644
index 00000000..509b9971
--- /dev/null
+++ b/src/populate_frames.h
@@ -0,0 +1,22 @@
+/* This code was taken from https://github.com/GoogleCloudPlatform/cloud-profiler-python/blob/main/googlecloudprofiler/src/populate_frames.h */ 
+
+#ifndef pp_frames
+#define pp_frames
+
+#include <Python.h>
+
+#include <frameobject.h>
+
+#define Py_BUILD_CORE
+#include "internal/pycore_frame.h"
+#undef Py_BUILD_CORE
+
+_PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame(PyThreadState *tstate);
+
+PyCodeObject *unsafe_PyInterpreterFrame_GetCode(_PyInterpreterFrame *frame);
+
+_PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack(_PyInterpreterFrame *frame);
+
+int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame);
+
+#endif
\ No newline at end of file
diff --git a/src/vmp_stack.c b/src/vmp_stack.c
index 0ea2b382..17909e6a 100644
--- a/src/vmp_stack.c
+++ b/src/vmp_stack.c
@@ -14,6 +14,11 @@
 #include "vmprof.h"
 #include "compat.h"
 
+#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
+#include "internal/pycore_frame.h"
+#include "populate_frames.h"
+#endif
+
 #ifdef VMP_SUPPORTS_NATIVE_PROFILING
 
 #if defined(VMPROF_LINUX) || defined(VMPROF_BSD)
@@ -84,6 +89,49 @@ int vmp_profiles_python_lines(void) {
     return _vmp_profiles_lines;
 }
 
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11 */
+    static _PyInterpreterFrame * _write_python_stack_entry(_PyInterpreterFrame * frame, void ** result, int * depth, int max_depth)
+    {
+#ifndef RPYTHON_VMPROF // pypy does not support line profiling
+    if (vmp_profiles_python_lines()) {
+        // In the line profiling mode we save a line number for every frame.
+        // Actual line number isn't stored in the frame directly (f_lineno
+        // points to the beginning of the frame), so we need to compute it
+        // from f_lasti and f_code->co_lnotab. Here is explained what co_lnotab
+        // is:
+        // https://svn.python.org/projects/python/trunk/Objects/lnotab_notes.txt
+
+        // NOTE: the profiling overhead can be reduced by storing co_lnotab in the dump and
+        // moving this computation to the reader instead of doing it here.
+        result[*depth] = (void*) (int64_t) _PyInterpreterFrame_GetLine(frame);
+        *depth = *depth + 1;
+    }
+    PyCodeObject* frame_code = unsafe_PyInterpreterFrame_GetCode(frame);
+    result[*depth] = (void*)CODE_ADDR_TO_UID(frame_code);
+    //Py_DECREF(frame_code);
+    *depth = *depth + 1;
+#else
+
+    if (frame->kind == VMPROF_CODE_TAG) {
+        int n = *depth;
+        result[n++] = (void*)frame->kind;
+        result[n++] = (void*)frame->value;
+        *depth = n;
+    }
+#ifdef PYPY_JIT_CODEMAP
+    else if (frame->kind == VMPROF_JITTED_TAG) {
+        intptr_t pc = ((intptr_t*)(frame->value - sizeof(intptr_t)))[0];
+        *depth = vmprof_write_header_for_jit_addr(result, *depth, pc, max_depth);
+    }
+#endif
+
+
+#endif
+
+    return unsafe_PyInterpreterFrame_GetBack(frame);
+}
+#else
+
 static PY_STACK_FRAME_T * _write_python_stack_entry(PY_STACK_FRAME_T * frame, void ** result, int * depth, int max_depth)
 {
 #ifndef RPYTHON_VMPROF // pypy does not support line profiling
@@ -100,7 +148,9 @@ static PY_STACK_FRAME_T * _write_python_stack_entry(PY_STACK_FRAME_T * frame, vo
         result[*depth] = (void*) (int64_t) PyFrame_GetLineNumber(frame);
         *depth = *depth + 1;
     }
-    result[*depth] = (void*)CODE_ADDR_TO_UID(FRAME_CODE(frame));
+    PyCodeObject* frame_code = FRAME_CODE(frame);
+    result[*depth] = (void*)CODE_ADDR_TO_UID(frame_code);
+    Py_DECREF(frame_code);
     *depth = *depth + 1;
 #else
 
@@ -123,14 +173,27 @@ static PY_STACK_FRAME_T * _write_python_stack_entry(PY_STACK_FRAME_T * frame, vo
     return FRAME_STEP(frame);
 }
 
-int vmp_walk_and_record_python_stack_only(PY_STACK_FRAME_T *frame, void ** result,
-                                          int max_depth, int depth, intptr_t pc)
-{
-    while ((depth + _per_loop()) <= max_depth && frame) {
-        frame = _write_python_stack_entry(frame, result, &depth, max_depth);
+#endif
+
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11 */
+    int vmp_walk_and_record_python_stack_only(_PyInterpreterFrame *frame, void ** result,
+                                            int max_depth, int depth, intptr_t pc)
+    {
+        while ((depth + _per_loop()) <= max_depth && frame) {
+            frame = _write_python_stack_entry(frame, result, &depth, max_depth);
+        }
+        return depth;
     }
-    return depth;
-}
+#else
+    int vmp_walk_and_record_python_stack_only(PY_STACK_FRAME_T *frame, void ** result,
+                                            int max_depth, int depth, intptr_t pc)
+    {
+        while ((depth + _per_loop()) <= max_depth && frame) {
+            frame = _write_python_stack_entry(frame, result, &depth, max_depth);
+        }
+        return depth;
+    }
+#endif
 
 #ifdef VMP_SUPPORTS_NATIVE_PROFILING
 int _write_native_stack(void* addr, void ** result, int depth, int max_depth) {
@@ -156,6 +219,161 @@ int _write_native_stack(void* addr, void ** result, int depth, int max_depth) {
 }
 #endif
 
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11 */
+int vmp_walk_and_record_stack(_PyInterpreterFrame *frame, void ** result,
+                              int max_depth, int signal, intptr_t pc) {
+
+    // called in signal handler
+    //
+    // This function records the stack trace for a python program. It also
+    // tracks native function calls if libunwind can be found on the system.
+    //
+    // The idea is the following (in the native case):
+    //
+    // 1) Remove frames until the signal frame is found (skipping it as well)
+    // 2) if the current frame corresponds to PyEval_EvalFrameEx (or the equivalent
+    //    for each python version), the jump to 4)
+    // 3) jump to 2)
+    // 4) walk each python frame and record it
+    //
+    //
+    // There are several cases that need to be taken care of.
+    //
+    // CPython supports line profiling, PyPy does not. At the same time
+    // PyPy saves the information of an address in the same way as line information
+    // is saved in CPython. _write_python_stack_entry for details.
+    //
+#ifdef VMP_SUPPORTS_NATIVE_PROFILING
+    void * func_addr;
+    unw_cursor_t cursor;
+    unw_context_t uc;
+    unw_proc_info_t pip;
+    int ret;
+
+    if (vmp_native_enabled() == 0) {
+        return vmp_walk_and_record_python_stack_only(frame, result, max_depth, 0, pc);
+    }
+
+    ret = unw_getcontext(&uc);
+    if (ret < 0) {
+        // could not initialize lib unwind cursor and context
+#if DEBUG
+        fprintf(stderr, "WARNING: unw_getcontext did not retreive context, switching to python profiling mode \n");
+#endif
+        vmp_native_disable();
+        return vmp_walk_and_record_python_stack_only(frame, result, max_depth, 0, pc);
+    }
+    ret = unw_init_local(&cursor, &uc);
+    if (ret < 0) {
+        // could not initialize lib unwind cursor and context
+#if DEBUG
+        fprintf(stderr, "WARNING: unw_init_local did not succeed, switching to python profiling mode \n");
+#endif
+        vmp_native_disable();
+        return vmp_walk_and_record_python_stack_only(frame, result, max_depth, 0, pc);
+    }
+
+    if (signal < 0) {
+        while (signal < 0) {
+            int err = unw_step(&cursor);
+            if (err <= 0) {
+#if DEBUG
+                fprintf(stderr, "WARNING: did not find signal frame, skipping sample\n");
+#endif
+                return 0;
+            }
+            signal++;
+        }
+    } else {
+#ifdef VMPROF_LINUX
+        while (signal) {
+            int is_signal_frame = unw_is_signal_frame(&cursor);
+            if (is_signal_frame) {
+                unw_step(&cursor); // step once more discard signal frame
+                break;
+            }
+            int err = unw_step(&cursor);
+            if (err <= 0) {
+#if DEBUG
+                fprintf(stderr,"WARNING: did not find signal frame, skipping sample\n");
+#endif
+                return 0;
+            }
+        }
+#else
+        // who would have guessed that unw_is_signal_frame does not work on mac os x
+        if (signal) {
+            unw_step(&cursor); // vmp_walk_and_record_stack
+            // get_stack_trace is inlined
+            unw_step(&cursor); // _vmprof_sample_stack
+            unw_step(&cursor); // sigprof_handler
+            unw_step(&cursor); // _sigtramp
+        }
+#endif
+    }
+
+    int depth = 0;
+    //PY_STACK_FRAME_T * top_most_frame = frame;
+    while ((depth + _per_loop()) <= max_depth) {
+        unw_get_proc_info(&cursor, &pip);
+
+        func_addr = (void*)pip.start_ip;
+
+        //{
+        //    char name[64];
+        //    unw_word_t x;
+        //    unw_get_proc_name(&cursor, name, 64, &x);
+        //    printf("  %s %p\n", name, func_addr);
+        //}
+
+        //if (func_addr == 0) {
+        //    unw_word_t rip = 0;
+        //    if (unw_get_reg(&cursor, UNW_REG_IP, &rip) < 0) {
+        //        printf("failed failed failed\n");
+        //    }
+        //    func_addr = rip;
+        //    printf("func_addr is 0, now %p\n", rip);
+        //}
+
+#ifdef PYPY_JIT_CODEMAP
+        long start_addr = 0;
+        unw_word_t rip = 0;
+        if (unw_get_reg(&cursor, UNW_REG_IP, &rip) < 0) {
+            return 0;
+        }
+#endif
+
+        if (IS_VMPROF_EVAL((void*)pip.start_ip)) {
+            // yes we found one stack entry of the python frames!
+            return vmp_walk_and_record_python_stack_only(frame, result, max_depth, depth, pc);
+#ifdef PYPY_JIT_CODEMAP
+        } else if (pypy_find_codemap_at_addr(rip, &start_addr) != NULL) {
+            depth = vmprof_write_header_for_jit_addr(result, depth, pc, max_depth);
+            return vmp_walk_and_record_python_stack_only(frame, result, max_depth, depth, pc);
+#endif
+        } else {
+            // mark native routines with the first bit set,
+            // this is possible because compiler align to 8 bytes.
+            //
+            if (func_addr != 0x0) {
+                depth = _write_native_stack((void*)(((uint64_t)func_addr) | 0x1), result, depth, max_depth);
+            }
+        }
+
+        int err = unw_step(&cursor);
+        if (err == 0) {
+            break;
+        } else if (err < 0) {
+            // this sample is broken, cannot walk native level... record python level (at least)
+            return vmp_walk_and_record_python_stack_only(frame, result, max_depth, 0, pc);
+        }
+    }
+
+    // if we come here, the found stack trace is removed and only python stacks are recorded
+#endif
+    return vmp_walk_and_record_python_stack_only(frame, result, max_depth, 0, pc);
+}
+#else
 int vmp_walk_and_record_stack(PY_STACK_FRAME_T *frame, void ** result,
                               int max_depth, int signal, intptr_t pc) {
 
@@ -309,6 +527,7 @@ int vmp_walk_and_record_stack(PY_STACK_FRAME_T *frame, void ** result,
 #endif
     return vmp_walk_and_record_python_stack_only(frame, result, max_depth, 0, pc);
 }
+#endif
 
 int vmp_native_enabled(void) {
 #ifdef VMP_SUPPORTS_NATIVE_PROFILING
diff --git a/src/vmp_stack.h b/src/vmp_stack.h
index 42031265..da4214c1 100644
--- a/src/vmp_stack.h
+++ b/src/vmp_stack.h
@@ -2,8 +2,18 @@
 
 #include "vmprof.h"
 
-int vmp_walk_and_record_stack(PY_STACK_FRAME_T * frame, void **data,
-                              int max_depth, int signal, intptr_t pc);
+#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
+#include "internal/pycore_frame.h"
+#include "populate_frames.h"
+#endif
+
+#if PY_VERSION_HEX >= 0x030B0000 /* >= 3.11 */
+    int vmp_walk_and_record_stack(_PyInterpreterFrame * frame, void **data,
+                                    int max_depth, int signal, intptr_t pc);
+#else
+    int vmp_walk_and_record_stack(PY_STACK_FRAME_T * frame, void **data,
+                                    int max_depth, int signal, intptr_t pc);
+#endif
 
 int vmp_native_enabled(void);
 int vmp_native_enable(void);
diff --git a/src/vmprof.h b/src/vmprof.h
index adc37cdf..ade3a0c1 100644
--- a/src/vmprof.h
+++ b/src/vmprof.h
@@ -75,7 +75,16 @@ int IS_VMPROF_EVAL(void * ptr);
 #define PY_EVAL_RETURN_T PyObject
 #define PY_THREAD_STATE_T PyThreadState
 #define FRAME_STEP(f) f->f_back
-#define FRAME_CODE(f) f->f_code
+
+#if PY_VERSION_HEX < 0x030900B1
+static inline PyCodeObject* PyFrame_GetCode(PyFrameObject *frame)
+{
+    Py_INCREF(frame->f_code);
+    return frame->f_code;
+}
+#endif
+
+#define FRAME_CODE(f) PyFrame_GetCode(f)
 
 #if CPYTHON_HAS_FRAME_EVALUATION
 #define IS_VMPROF_EVAL(PTR) PTR == (void*)_PyEval_EvalFrameDefault
diff --git a/src/vmprof_unix.c b/src/vmprof_unix.c
index 3bd17b16..808610f3 100644
--- a/src/vmprof_unix.c
+++ b/src/vmprof_unix.c
@@ -1,5 +1,9 @@
 #include "vmprof_unix.h"
 
+#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
+#include "populate_frames.h"
+#endif
+
 #ifdef VMPROF_UNIX
 
 #if VMPROF_LINUX
@@ -475,9 +479,22 @@ int vmprof_register_virtual_function(char *code_name, intptr_t code_uid,
     return 0;
 }
 
+#if PY_VERSION_HEX < 0x030900B1 /* < 3.9 */
+static inline PyFrameObject* PyThreadState_GetFrame(PyThreadState *tstate)
+{
+    Py_XINCREF(tstate->frame);
+    return tstate->frame;
+}
+#endif
+
 int get_stack_trace(PY_THREAD_STATE_T * current, void** result, int max_depth, intptr_t pc)
 {
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11, no pypy 3.11 at the moment*/ 
+    _PyInterpreterFrame * frame;
+#else
     PY_STACK_FRAME_T * frame;
+#endif
+
 #ifdef RPYTHON_VMPROF
     // do nothing here,
     frame = (PY_STACK_FRAME_T*)current;
@@ -488,7 +505,13 @@ int get_stack_trace(PY_THREAD_STATE_T * current, void** result, int max_depth, i
 #endif
         return 0;
     }
-    frame = current->frame;
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11 */
+    frame = unsafe_PyThreadState_GetInterpreterFrame(current);
+#else
+    frame = PyThreadState_GetFrame(current);
+#endif
+   
+
 #endif
     if (frame == NULL) {
 #if DEBUG
@@ -496,5 +519,12 @@ int get_stack_trace(PY_THREAD_STATE_T * current, void** result, int max_depth, i
 #endif
         return 0;
     }
-    return vmp_walk_and_record_stack(frame, result, max_depth, 1, pc);
+
+    int res = vmp_walk_and_record_stack(frame, result, max_depth, 1, pc);
+
+#if PY_VERSION_HEX < 0x030B0000 /* < 3.11 */
+    Py_XDECREF(frame);
+#endif
+
+    return res;
 }
diff --git a/src/vmprof_win.c b/src/vmprof_win.c
index daadd98f..a00e113d 100644
--- a/src/vmprof_win.c
+++ b/src/vmprof_win.c
@@ -1,5 +1,10 @@
 #include "vmprof_win.h"
 
+#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
+#include "internal/pycore_frame.h"
+#include "populate_frames.h"
+#endif
+
 volatile int thread_started = 0;
 volatile int enabled = 0;
 
@@ -53,6 +58,14 @@ int vmp_write_all(const char *buf, size_t bufsize)
     return 0;
 }
 
+#if PY_VERSION_HEX < 0x030900B1 /* < 3.9 */
+static inline PyFrameObject* PyThreadState_GetFrame(PyThreadState *tstate)
+{
+    Py_XINCREF(tstate->frame);
+    return tstate->frame;
+}
+#endif
+
 HANDLE write_mutex;
 
 #include "vmprof_common.h"
@@ -90,8 +103,19 @@ int vmprof_snapshot_thread(DWORD thread_id, PY_WIN_THREAD_STATE *tstate, prof_st
     ResumeThread(hThread);
     return depth;
 #else
-    depth = vmp_walk_and_record_stack(tstate->frame, stack->stack,
+
+#if PY_VERSION_HEX >= 0x030B0000 /* < 3.11 */
+    _PyInterpreterFrame * frame = unsafe_PyThreadState_GetInterpreterFrame(tstate);
+#else
+    PY_STACK_FRAME_T * frame = PyThreadState_GetFrame(tstate);
+#endif
+
+    depth = vmp_walk_and_record_stack(frame, stack->stack,
                                       MAX_STACK_DEPTH, 0, 0);
+
+#if PY_VERSION_HEX < 0x030B0000 /* < 3.11 */
+    Py_XDECREF(frame);
+#endif
     stack->depth = depth;
     stack->stack[depth++] = (void*)((ULONG_PTR)thread_id);
     stack->count = 1;


Test patch:
diff --git a/test_requirements.txt b/test_requirements.txt
index 6d544044..6ef21c72 100644
--- a/test_requirements.txt
+++ b/test_requirements.txt
@@ -7,3 +7,4 @@ hypothesis
 colorama
 pytz
 attrs
+py
diff --git a/vmprof/test/test_c_source.py b/vmprof/test/test_c_source.py
index 0e46fcd4..0d609cbf 100644
--- a/vmprof/test/test_c_source.py
+++ b/vmprof/test/test_c_source.py
@@ -30,10 +30,13 @@ def setup_class(cls):
                 if platform.machine() == 'x86_64':
                     libs.append('unwind-x86_64')
             # trick: compile with _CFFI_USE_EMBEDDING=1 which will not define Py_LIMITED_API
+            sources = []
+            if sys.version_info[:2] == (3,11):
+                sources += ['src/populate_frames.c']# needed for cp311 but must not be included in py < 3.11
             stack_ffi.set_source("vmprof.test._test_stack", source, include_dirs=['src'],
                                  define_macros=[('_CFFI_USE_EMBEDDING',1), ('PY_TEST',1),
                                                 ('VMP_SUPPORTS_NATIVE_PROFILING',1)],
-                                 libraries=libs, extra_compile_args=['-Werror', '-g'])
+                                 libraries=libs, extra_compile_args=['-Werror', '-g'], sources=sources)
 
         stack_ffi.compile(verbose=True)
         from vmprof.test import _test_stack as clib