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 Jul 7, 2024
1 parent 4559915 commit e224e5b
Show file tree
Hide file tree
Showing 15 changed files with 303 additions and 64 deletions.
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
2 changes: 1 addition & 1 deletion 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
97 changes: 82 additions & 15 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, "xdebugpy", 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;

Check warning on line 250 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L247-L250

Added lines #L247 - L250 were not covered by tests

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

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

Check warning on line 254 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L254

Added line #L254 was not covered by tests

init_version_descriptor(self->py_v, &py_d);

Check warning on line 256 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L256

Added line #L256 was not covered by tests

SUCCESS;

Check warning on line 258 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L258

Added line #L258 was not covered by tests
}
}

// 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 @@ -560,6 +581,45 @@ _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);
}

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

Check warning on line 596 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L596

Added line #L596 was not covered by tests
}

self->is = calloc(1, self->py_v->py_is.size);
if (!isvalid(self->is)) {
log_e("Cannot allocate memory for PyInterpreterState");
goto error;

Check warning on line 602 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L601-L602

Added lines #L601 - L602 were not covered by tests
}

self->ts = calloc(1, self->py_v->py_thread.size);
if (!isvalid(self->ts)) {
log_e("Cannot allocate memory for PyThreadState");
goto error;

Check warning on line 608 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L607-L608

Added lines #L607 - L608 were not covered by tests
}

log_d("Local buffers initialised");

SUCCESS;

error:

Check warning on line 615 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L615

Added line #L615 was not covered by tests
set_error(ENOMEM);

_py_proc__free_local_buffers(self);

Check warning on line 618 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L618

Added line #L618 was not covered by tests

FAIL;

Check warning on line 620 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L620

Added line #L620 was not covered by tests
}

// ----------------------------------------------------------------------------
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;

Check warning on line 639 in src/py_proc.c

View check run for this annotation

Codecov / codecov/patch

src/py_proc.c#L639

Added line #L639 was not covered by tests

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,10 @@ 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;

_prehash_symbols();

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
17 changes: 17 additions & 0 deletions src/py_proc.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ typedef struct {
hash_table_t * base_table;
#endif

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

// Platform-dependent fields
proc_extra_info * extra;
} py_proc_t;
Expand Down Expand Up @@ -208,6 +212,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
18 changes: 9 additions & 9 deletions src/py_thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ static inline int
_py_thread__unwind_iframe_stack(py_thread_t * self, void * iframe_raddr) {
int invalid = FALSE;
void * curr = iframe_raddr;

while (isvalid(curr)) {
if (fail(_py_thread__push_iframe(self, &curr))) {
log_d("Failed to retrieve iframe #%d", stack_pointer());
Expand Down Expand Up @@ -457,8 +457,6 @@ static inline int
_py_thread__unwind_cframe_stack(py_thread_t * self) {
PyCFrame cframe;

int invalid = FALSE;

_py_thread__read_stack(self);

stack_reset();
Expand All @@ -470,11 +468,7 @@ _py_thread__unwind_cframe_stack(py_thread_t * self) {
FAIL;
}

invalid = fail(_py_thread__unwind_iframe_stack(self, V_FIELD(void *, cframe, py_cframe, o_current_frame)));
if (invalid)
return invalid;

return invalid;
return fail(_py_thread__unwind_iframe_stack(self, V_FIELD(void *, cframe, py_cframe, o_current_frame)));
}


Expand Down Expand Up @@ -954,7 +948,13 @@ py_thread__emit_collapsed_stack(py_thread_t * self, int64_t interp_id, ctime_t t
V_DESC(self->proc->py_v);

if (isvalid(self->top_frame)) {
if (V_MIN(3, 11)) {
if (V_MIN(3, 13)) {
if (fail(_py_thread__unwind_iframe_stack(self, self->top_frame))) {
emit_invalid_frame();
error = TRUE;

Check warning on line 954 in src/py_thread.c

View check run for this annotation

Codecov / codecov/patch

src/py_thread.c#L954

Added line #L954 was not covered by tests
}
}
else if (V_MIN(3, 11)) {
if (fail(_py_thread__unwind_cframe_stack(self))) {
emit_invalid_frame();
error = TRUE;
Expand Down
Loading

0 comments on commit e224e5b

Please sign in to comment.