Skip to content

Commit

Permalink
feat: add support for CPython 3.13
Browse files Browse the repository at this point in the history
We add support for CPython 3.13
  • Loading branch information
P403n1x87 committed Oct 13, 2024
1 parent 7c42586 commit a67e572
Show file tree
Hide file tree
Showing 16 changed files with 354 additions and 72 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ jobs:
sudo apt-get update
sudo apt-get -y install \
valgrind \
python3.{8..12} \
python3.{8..13} \
python3.10-full python3.10-dev
python3.10 -m venv .venv
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

env:
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}
Expand Down Expand Up @@ -132,7 +132,7 @@ jobs:
- name: Run functional Austin tests (with sudo)
run: |
ulimit -c unlimited
echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern
sudo echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern
sudo -E env PATH="$PATH" .venv/bin/pytest --pastebin=failed -svr a test/functional -k "not austinp"
if: always()

Expand Down Expand Up @@ -259,7 +259,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

env:
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}
Expand Down Expand Up @@ -385,7 +385,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

env:
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}
Expand Down Expand Up @@ -474,7 +474,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]

env:
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}
Expand Down
4 changes: 3 additions & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
2024-xx-xx v3.6.1
2024-xx-xx v3.7.0

Added support for CPython 3.13.

Improve support for Python processes running in containers.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ folder in either the SVG, PDF or PNG format

# Compatibility

Austin supports Python 3.8 through 3.12, and has been tested on the following
Austin supports Python 3.8 through 3.13, and has been tested on the following
platforms and architectures

| | <img src="art/tux.svg" /> | <img src="art/win.svg"/> | <img src="art/apple.svg"/> |
Expand Down
2 changes: 1 addition & 1 deletion configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ AC_PREREQ([2.69])
# from scripts.utils import get_current_version_from_changelog as version
# print(f"AC_INIT([austin], [{version()}], [https://github.com/p403n1x87/austin/issues])")
# ]]]
AC_INIT([austin], [3.6.1], [https://github.com/p403n1x87/austin/issues])
AC_INIT([austin], [3.7.0], [https://github.com/p403n1x87/austin/issues])
# [[[end]]]
AC_CONFIG_SRCDIR([config.h.in])
AC_CONFIG_HEADERS([config.h])
Expand Down
4 changes: 2 additions & 2 deletions doc/cheatsheet.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions scripts/build-wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
"Project-URL": [
"Homepage, https://github.com/P403n1x87/austin",
Expand Down
2 changes: 1 addition & 1 deletion snap/snapcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ base: core20
# from scripts.utils import get_current_version_from_changelog as version
# print(f"version: '{version()}+git'")
# ]]]
version: '3.6.1+git'
version: '3.7.0+git'
# [[[end]]]
summary: A Python frame stack sampler for CPython
description: |
Expand Down
2 changes: 1 addition & 1 deletion src/austin.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from scripts.utils import get_current_version_from_changelog as version
print(f'#define VERSION "{version()}"')
]]] */
#define VERSION "3.6.1"
#define VERSION "3.7.0"
// [[[end]]]

#endif
109 changes: 88 additions & 21 deletions src/py_proc.c
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,30 @@ _py_proc__infer_python_version(py_proc_t * self) {

int major = 0, minor = 0, patch = 0;

// Starting with Python 3.13 we can use the PyRuntime structure
if (isvalid(self->symbols[DYNSYM_RUNTIME])) {
_Py_DebugOffsets py_d;
if (fail(py_proc__get_type(self, self->symbols[DYNSYM_RUNTIME], py_d))) {
log_e("Cannot copy PyRuntimeState structure from remote address");
FAIL;
}

if (0 == memcmp(py_d.v3_13.cookie, _Py_Debug_Cookie, sizeof(py_d.v3_13.cookie))) {
uint64_t version = py_d.v3_13.version;
major = (version>>24) & 0xFF;
minor = (version>>16) & 0xFF;
patch = (version>>8) & 0xFF;

log_d("Python version (from debug offsets): %d.%d.%d", major, minor, patch);

self->py_v = get_version_descriptor(major, minor, patch);

init_version_descriptor(self->py_v, &py_d);

SUCCESS;
}
}

// Starting with Python 3.11 we can rely on the Py_Version symbol
if (isvalid(self->symbols[DYNSYM_HEX_VERSION])) {
unsigned long py_version = 0;
Expand Down Expand Up @@ -322,17 +346,14 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {

V_DESC(self->py_v);

PyInterpreterState is;
PyThreadState tstate_head;

if (py_proc__get_type(self, raddr, is)) {
if (py_proc__copy_v(self, is, raddr, self->is)) {
log_ie("Cannot get remote interpreter state");
FAIL;
}
log_d("Interpreter state buffer %p", self->is);
void * tstate_head_addr = V_FIELD_PTR(void *, self->is, py_is, o_tstate_head);

void * tstate_head_addr = V_FIELD(void *, is, py_is, o_tstate_head);

if (fail(py_proc__get_type(self, tstate_head_addr, tstate_head))) {
if (fail(py_proc__copy_v(self, thread, tstate_head_addr, self->ts))) {
log_e(
"Cannot copy PyThreadState head at %p from PyInterpreterState instance",
tstate_head_addr
Expand All @@ -342,7 +363,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {

log_t("PyThreadState head loaded @ %p", V_FIELD(void *, is, py_is, o_tstate_head));

if (V_FIELD(void*, tstate_head, py_thread, o_interp) != raddr) {
if (V_FIELD_PTR(void*, self->ts, py_thread, o_interp) != raddr) {
log_d("PyThreadState head does not point to interpreter state");
set_error(EPROC);
FAIL;
Expand All @@ -358,7 +379,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
raddr, V_FIELD(void *, is, py_is, o_tstate_head)
);

raddr_t thread_raddr = {self->proc_ref, V_FIELD(void *, is, py_is, o_tstate_head)};
raddr_t thread_raddr = {self->proc_ref, V_FIELD_PTR(void *, self->is, py_is, o_tstate_head)};
py_thread_t thread;

if (fail(py_thread__fill_from_raddr(&thread, &thread_raddr, self))) {
Expand Down Expand Up @@ -489,7 +510,6 @@ _py_proc__deref_interp_head(py_proc_t * self) {

void * interp_head_raddr = NULL;

_PyRuntimeState py_runtime;
void * runtime_addr = self->symbols[DYNSYM_RUNTIME];
#if defined PL_LINUX
const size_t size = getpagesize();
Expand All @@ -510,15 +530,15 @@ _py_proc__deref_interp_head(py_proc_t * self) {
#endif

for (void * current_addr = lower; current_addr <= upper; current_addr += sizeof(void *)) {
if (py_proc__get_type(self, current_addr, py_runtime)) {
if (py_proc__copy_v(self, runtime, current_addr, self->rs)) {
log_d(
"Cannot copy runtime state structure from remote address %p",
current_addr
);
continue;
}

interp_head_raddr = V_FIELD(void *, py_runtime, py_runtime, o_interp_head);
interp_head_raddr = V_FIELD_PTR(void *, self->rs, py_runtime, o_interp_head);
if (V_MAX(3, 8)) {
self->gc_state_raddr = current_addr + py_v->py_runtime.o_gc;
log_d("GC runtime state @ %p", self->gc_state_raddr);
Expand Down Expand Up @@ -560,6 +580,46 @@ _py_proc__get_current_thread_state_raddr(py_proc_t * self) {
return (void *) -1;
}

// ----------------------------------------------------------------------------
static void
_py_proc__free_local_buffers(py_proc_t * self) {
sfree(self->is);
sfree(self->ts);
sfree(self->rs);
}

// ----------------------------------------------------------------------------
#define LOCAL_ALLOC(dest, src, name) { \
self->dest = calloc(1, self->py_v->py_##src.size); \
if (!isvalid(self->dest)) { \
log_e("Cannot allocate memory for " #name); \
goto error; \
} \
}

static int
_py_proc__init_local_buffers(py_proc_t * self) {
if (!isvalid(self)) {
set_error(EPROC);
FAIL;
}

LOCAL_ALLOC(rs, runtime, "PyRuntimeState");
LOCAL_ALLOC(is, is, "PyInterpreterState");
LOCAL_ALLOC(ts, thread, "PyThreadState");

log_d("Local buffers initialised");

SUCCESS;

error:
set_error(ENOMEM);

_py_proc__free_local_buffers(self);

FAIL;
}

// ----------------------------------------------------------------------------
static int
_py_proc__find_interpreter_state(py_proc_t * self) {
Expand All @@ -575,6 +635,9 @@ _py_proc__find_interpreter_state(py_proc_t * self) {
if (fail(_py_proc__infer_python_version(self)))
FAIL;

if (fail(_py_proc__init_local_buffers(self)))
FAIL;

if (self->sym_loaded || isvalid(self->map.runtime.base)) {
// Try to resolve the symbols or the runtime section, if we have them

Expand Down Expand Up @@ -703,6 +766,11 @@ py_proc_new(int child) {

py_proc->child = child;
py_proc->gc_state_raddr = NULL;
py_proc->py_v = NULL;

py_proc->is = NULL;
py_proc->ts = NULL;
py_proc->rs = NULL;

_prehash_symbols();

Expand Down Expand Up @@ -951,12 +1019,11 @@ _py_proc__find_current_thread_offset(py_proc_t * self, void * thread_raddr) {
V_DESC(self->py_v);

void * interp_head_raddr;
_PyRuntimeState py_runtime;

if (py_proc__get_type(self, self->symbols[DYNSYM_RUNTIME], py_runtime))
if (py_proc__copy_v(self, runtime, self->symbols[DYNSYM_RUNTIME], self->rs))
FAIL;

interp_head_raddr = V_FIELD(void *, py_runtime, py_runtime, o_interp_head);
interp_head_raddr = V_FIELD_PTR(void *, self->rs, py_runtime, o_interp_head);

// Search offset of current thread in _PyRuntimeState structure
PyInterpreterState is;
Expand Down Expand Up @@ -1200,15 +1267,13 @@ py_proc__sample(py_proc_t * self) {

V_DESC(self->py_v);

PyInterpreterState is;

do {
if (fail(py_proc__get_type(self, current_interp, is))) {
if (fail(py_proc__copy_v(self, is, current_interp, self->is))) {
log_ie("Failed to get interpreter state while sampling");
FAIL;
}

void * tstate_head = V_FIELD(void *, is, py_is, o_tstate_head);
void * tstate_head = V_FIELD_PTR(void *, self->is, py_is, o_tstate_head);
if (!isvalid(tstate_head))
// Maybe the interpreter state is in an invalid state. We'll try again
// unless there is a fatal error.
Expand All @@ -1223,7 +1288,7 @@ py_proc__sample(py_proc_t * self) {
time_delta = gettime() - self->timestamp;
#endif

int result = _py_proc__sample_interpreter(self, &is, time_delta);
int result = _py_proc__sample_interpreter(self, self->is, time_delta);

#ifdef NATIVE
if (fail(_py_proc__resume_threads(self, &raddr))) {
Expand All @@ -1234,7 +1299,7 @@ py_proc__sample(py_proc_t * self) {

if (fail(result))
FAIL;
} while (isvalid(current_interp = V_FIELD(void *, is, py_is, o_next)));
} while (isvalid(current_interp = V_FIELD_PTR(void *, self->is, py_is, o_next)));

#ifdef NATIVE
self->timestamp = gettime();
Expand Down Expand Up @@ -1321,6 +1386,8 @@ py_proc__destroy(py_proc_t * self) {
hash_table__destroy(self->base_table);
#endif

_py_proc__free_local_buffers(self);

sfree(self->bin_path);
sfree(self->lib_path);
sfree(self->extra);
Expand Down
18 changes: 18 additions & 0 deletions src/py_proc.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ typedef struct {
hash_table_t * base_table;
#endif

// Local buffers
_PyRuntimeState * rs;
PyInterpreterState * is;
PyThreadState * ts;

// Platform-dependent fields
proc_extra_info * extra;
} py_proc_t;
Expand Down Expand Up @@ -208,6 +213,19 @@ py_proc__sample(py_proc_t *);
*/
#define py_proc__get_type(self, raddr, dt) (py_proc__memcpy(self, raddr, sizeof(dt), &dt))

/**
* Make a local copy of a remote structure.
*
* @param self the process object.
* @param type the type of the structure.
* @param raddr the remote address of the structure.
* @param dest the destination address.
*
* @return 0 on success.
*/
#define py_proc__copy_v(self, type, raddr, dest) (py_proc__memcpy(self, raddr, py_v->py_##type.size, dest))


/**
* Log the Python interpreter version
* @param self the process object.
Expand Down
2 changes: 1 addition & 1 deletion src/py_proc_list.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ py_proc_list__sample(py_proc_list_t * self) {
for (py_proc_item_t * item = self->first; item != NULL; /* item = item->next */) {
log_t("Sampling process with PID %d", item->py_proc->pid);
stopwatch_start();
if (fail(py_proc__sample(item->py_proc))) {
if (!isvalid(item->py_proc->py_v) || fail(py_proc__sample(item->py_proc))) {
py_proc__wait(item->py_proc);
py_proc_item_t * next = item->next;
_py_proc_list__remove(self, item);
Expand Down
Loading

0 comments on commit a67e572

Please sign in to comment.