Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
matrix:
# Test all supported versions on Ubuntu:
os: [ubuntu-latest]
python: ["3.9", "3.10", "3.11", "pypy-3.10"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy-3.10"]
experimental: [false]
# include:
# - os: macos-latest
Expand Down
49 changes: 32 additions & 17 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
from setuptools import setup, find_packages, Extension
from distutils.command.build_py import build_py
from setuptools.command.build_py import build_py
from setuptools.command.build_ext import build_ext
import os, sys
import subprocess
import platform

IS_PYPY = '__pypy__' in sys.builtin_module_names

BASEDIR = os.path.dirname(os.path.abspath(__file__))

class vmprof_build(build_py, object):
def run(self):
super(vmprof_build, self).run()

BASEDIR = os.path.dirname(os.path.abspath(__file__))
class vmprof_build_ext(build_ext, object):
"""build_ext that runs libbacktrace configure before building.
This is needed because libbacktrace does not have a pre-built library for all platforms.
"""
def run(self):
# configure libbacktrace on Unix systems (not Windows/macOS)
if sys.platform.startswith('linux') or sys.platform.startswith('freebsd'):
libbacktrace_dir = os.path.join(BASEDIR, "src", "libbacktrace")
config_h = os.path.join(libbacktrace_dir, "config.h")
# only run configure if config.h doesn't exist
if not os.path.exists(config_h):
orig_dir = os.getcwd()
os.chdir(libbacktrace_dir)
try:
# generate configure script if it doesn't exist
if not os.path.exists("configure"):
subprocess.check_call(["autoreconf", "-i"])
subprocess.check_call(["./configure"])
finally:
os.chdir(orig_dir)
super(vmprof_build_ext, self).run()

def _supported_unix():
if sys.platform.startswith('linux'):
Expand Down Expand Up @@ -65,20 +87,13 @@ def _supported_unix():
'src/libbacktrace/posix.c',
'src/libbacktrace/sort.c',
]
# configure libbacktrace!!
class vmprof_build(build_py, object):
def run(self):
orig_dir = os.getcwd()
os.chdir(os.path.join(BASEDIR, "src", "libbacktrace"))
subprocess.check_call(["./configure"])
os.chdir(orig_dir)
super(vmprof_build, self).run()

else:
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):
# use absolute paths for include directories so compilation works from any directory
extra_compile_args.append('-I' + os.path.join(BASEDIR, 'src'))
extra_compile_args.append('-I' + os.path.join(BASEDIR, 'src', 'libbacktrace'))
if sys.version_info[:2] >= (3,11):
extra_source_files += ['src/populate_frames.c']
ext_modules = [Extension('_vmprof',
sources=[
Expand Down Expand Up @@ -116,14 +131,14 @@ def run(self):
description="Python's vmprof client",
long_description='See https://vmprof.readthedocs.org/',
url='https://github.com/vmprof/vmprof-python',
cmdclass={'build_py': vmprof_build},
cmdclass={'build_py': vmprof_build, 'build_ext': vmprof_build_ext},
install_requires=[
'requests',
'six',
'pytz',
'colorama',
] + extra_install_requires,
python_requires='<3.12',
python_requires='<3.15',
tests_require=['pytest','cffi','hypothesis'],
entry_points = {
'console_scripts': [
Expand All @@ -140,4 +155,4 @@ def run(self):
zip_safe=False,
include_package_data=True,
ext_modules=ext_modules,
)
)
5 changes: 4 additions & 1 deletion src/_vmprof.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

#ifndef RPYTHON_VMPROF
#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
#include "internal/pycore_frame.h"
#include "populate_frames.h"
#endif
#endif
Expand Down Expand Up @@ -140,7 +139,11 @@ void emit_all_code_objects(PyObject * seen_code_ids)
Py_ssize_t i, size;
void * param[2];

#if PY_VERSION_HEX >= 0x030D0000 /* >= 3.13 */
gc_module = PyImport_ImportModule("gc");
#else
gc_module = PyImport_ImportModuleNoBlock("gc");
#endif
if (gc_module == NULL)
goto error;

Expand Down
28 changes: 27 additions & 1 deletion src/populate_frames.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

// 0x030B0000 is 3.11.
#define PY_311 0x030B0000
// 0x030D0000 is 3.13.
#define PY_313 0x030D0000
// 0x030E0000 is 3.14.
#define PY_314 0x030E0000

#if PY_VERSION_HEX >= PY_311

/**
Expand All @@ -22,15 +27,26 @@
*/

#define Py_BUILD_CORE
#if PY_VERSION_HEX >= PY_314
// Python 3.14 moved frame internals to pycore_interpframe.h
#include "internal/pycore_interpframe.h"
#else
#include "internal/pycore_frame.h"
#endif
#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);
#if PY_VERSION_HEX >= PY_313
// In Python 3.13+, cframe was removed and current_frame is directly on tstate
_PyInterpreterFrame *f = tstate->current_frame;
#else
// Python 3.11 and 3.12 use cframe->current_frame
_PyInterpreterFrame *f = tstate->cframe->current_frame;
#endif
while (f && _PyFrame_IsIncomplete(f)) {
f = f->previous;
}
Expand All @@ -47,7 +63,13 @@ PyCodeObject *unsafe_PyInterpreterFrame_GetCode(
_PyInterpreterFrame *frame) {
assert(frame != NULL);
assert(!_PyFrame_IsIncomplete(frame));
#if PY_VERSION_HEX >= PY_313
// In Python 3.13+, use the _PyFrame_GetCode inline function
// f_code was renamed to f_executable
PyCodeObject *code = _PyFrame_GetCode(frame);
#else
PyCodeObject *code = frame->f_code;
#endif
assert(code != NULL);
return code;
}
Expand All @@ -71,6 +93,10 @@ _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack(
// this function is not available in libpython
int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame) {
int addr = _PyInterpreterFrame_LASTI(frame) * sizeof(_Py_CODEUNIT);
#if PY_VERSION_HEX >= PY_313
return PyCode_Addr2Line(_PyFrame_GetCode(frame), addr);
#else
return PyCode_Addr2Line(frame->f_code, addr);
#endif
}
#endif // PY_VERSION_HEX >= PY_311
#endif // PY_VERSION_HEX >= PY_311
10 changes: 9 additions & 1 deletion src/populate_frames.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@

#include <frameobject.h>

// 0x030E0000 is 3.14.
#define PY_314 0x030E0000

#define Py_BUILD_CORE
#if PY_VERSION_HEX >= PY_314
// Python 3.14 moved frame internals to pycore_interpframe.h
#include "internal/pycore_interpframe.h"
#else
#include "internal/pycore_frame.h"
#endif
#undef Py_BUILD_CORE

_PyInterpreterFrame *unsafe_PyThreadState_GetInterpreterFrame(PyThreadState *tstate);
Expand All @@ -19,4 +27,4 @@ _PyInterpreterFrame *unsafe_PyInterpreterFrame_GetBack(_PyInterpreterFrame *fram

int _PyInterpreterFrame_GetLine(_PyInterpreterFrame *frame);

#endif
#endif
48 changes: 46 additions & 2 deletions src/vmp_stack.c
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,50 @@ PY_EVAL_RETURN_T * vmprof_eval(PY_STACK_FRAME_T *f, int throwflag) { return NULL
static intptr_t *vmp_ranges = NULL;
static ssize_t vmp_range_count = 0;
static int vmp_native_traces_enabled = 0;

/**
* Check if the given function is a Python eval frame function.
*
* On Python 3.13, the direct pointer comparison with _PyEval_EvalFrameDefault
* may fail due to internal interpreter changes. This function provides a fallback
* by checking the function name using libunwind.
*
* @param pip Pointer to the procedure info from libunwind
* @param cursor Pointer to the libunwind cursor (for name lookup fallback)
* @return 1 if this is an eval frame, 0 otherwise
*/
static int is_vmprof_eval_frame(unw_proc_info_t *pip, unw_cursor_t *cursor) {
// First try fast pointer comparison (works on most Python versions)
if (IS_VMPROF_EVAL((void*)pip->start_ip)) {
return 1;
}

#if PY_VERSION_HEX >= 0x030B0000 /* Python 3.11+ needs name-based fallback */
// On Python 3.11+, the pointer comparison may fail due to interpreter changes.
// Technically needed only on 3.13, yet safe to use on all 3.11+.
// Fall back to checking the function name.
char proc_name[128];
unw_word_t offset;

if (unw_get_proc_name(cursor, proc_name, sizeof(proc_name), &offset) == 0) {
// Check for known Python eval frame function names
// _PyEval_EvalFrameDefault is the main eval function since Python 3.6
if (strstr(proc_name, "_PyEval_EvalFrameDefault") != NULL) {
return 1;
}
// PyEval_EvalCode is the entry point for code evaluation
if (strstr(proc_name, "PyEval_EvalCode") != NULL) {
return 1;
}
// Also check for potential variants or wrappers
if (strstr(proc_name, "PyEval_EvalFrame") != NULL) {
return 1;
}
}
#endif

return 0;
}
#endif
static int _vmp_profiles_lines = 0;

Expand Down Expand Up @@ -338,7 +382,7 @@ int vmp_walk_and_record_stack(_PyInterpreterFrame *frame, void ** result,
}
#endif

if (IS_VMPROF_EVAL((void*)pip.start_ip)) {
if (is_vmprof_eval_frame(&pip, &cursor)) {
// 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
Expand Down Expand Up @@ -492,7 +536,7 @@ int vmp_walk_and_record_stack(PY_STACK_FRAME_T *frame, void ** result,
}
#endif

if (IS_VMPROF_EVAL((void*)pip.start_ip)) {
if (is_vmprof_eval_frame(&pip, &cursor)) {
// 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
Expand Down
6 changes: 6 additions & 0 deletions src/vmp_stack.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

#ifndef RPYTHON_VMPROF
#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
#define Py_BUILD_CORE
#if PY_VERSION_HEX >= 0x030E0000 /* >= 3.14 */
#include "internal/pycore_interpframe.h"
#else
#include "internal/pycore_frame.h"
#endif
#undef Py_BUILD_CORE
#include "populate_frames.h"
#endif
#endif
Expand Down
12 changes: 11 additions & 1 deletion src/vmprof_win.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

#ifndef RPYTHON_VMPROF
#if PY_VERSION_HEX >= 0x030b00f0 /* >= 3.11 */
#include "internal/pycore_frame.h"
#include "populate_frames.h"
#endif
#endif

volatile int thread_started = 0;
volatile int enabled = 0;
#ifndef RPYTHON_VMPROF
static PY_WIN_THREAD_STATE *target_tstate = NULL;
#endif

HANDLE write_mutex;

Expand Down Expand Up @@ -174,6 +176,8 @@ long __stdcall vmprof_mainloop(void *arg)
continue;
}
tstate = get_current_thread_state();
if (!tstate)
tstate = target_tstate;
if (!tstate)
continue;
depth = vmprof_snapshot_thread(tstate->thread_id, tstate, stack);
Expand Down Expand Up @@ -221,6 +225,9 @@ int vmprof_enable(int memory, int native, int real_time)
thread_started = 1;
}
enabled = 1;
#ifndef RPYTHON_VMPROF
target_tstate = PyThreadState_Get();
#endif
return 0;
}

Expand All @@ -231,6 +238,9 @@ int vmprof_disable(void)
(void)vmp_write_time_now(MARKER_TRAILER);

enabled = 0;
#ifndef RPYTHON_VMPROF
target_tstate = NULL;
#endif
vmp_set_profile_fileno(-1);
return 0;
}
Expand Down
2 changes: 1 addition & 1 deletion vmprof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class IniParser(object):

def __init__(self, f):
self.ini_parser = configparser.ConfigParser()
self.ini_parser.readfp(f)
self.ini_parser.read_file(f)

def get_option(self, name, type, default=None):
if type == float:
Expand Down
4 changes: 2 additions & 2 deletions vmprof/test/test_c_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def setup_class(cls):
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
if sys.version_info[:2] >= (3, 11):
sources += ['src/populate_frames.c']# needed for py 3.11+ 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)],
Expand Down
9 changes: 7 additions & 2 deletions vmprof/test/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,13 @@ def test_nested_call():
assert len(t.children) == 1
assert 'function_foo' in t[''].name
if PY3K:
assert len(t[''].children) == 1
assert '<listcomp>' in t[''][''].name
# In Python 3.12+, list comprehensions are inlined and don't create
# a separate stack frame (PEP 709), so <listcomp> won't appear
if sys.version_info >= (3, 12):
assert len(t[''].children) == 0
else:
assert len(t[''].children) == 1
assert '<listcomp>' in t[''][''].name
else:
assert len(t[''].children) == 0

Expand Down
Loading