Skip to content

Commit 05875d0

Browse files
committed
feat: add support for CPython 3.13
We add support for CPython 3.13
1 parent 741d378 commit 05875d0

16 files changed

+356
-39
lines changed

.github/workflows/checks.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ jobs:
156156
sudo apt-get update
157157
sudo apt-get -y install \
158158
valgrind \
159-
python3.{8..12} \
159+
python3.{8..13} \
160160
python3.10-full python3.10-dev
161161
162162
python3.10 -m venv .venv

.github/workflows/tests.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ jobs:
7575
strategy:
7676
fail-fast: false
7777
matrix:
78-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
78+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
7979

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

@@ -259,7 +259,7 @@ jobs:
259259
strategy:
260260
fail-fast: false
261261
matrix:
262-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
262+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
263263

264264
env:
265265
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}
@@ -385,7 +385,7 @@ jobs:
385385
strategy:
386386
fail-fast: false
387387
matrix:
388-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
388+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
389389

390390
env:
391391
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}
@@ -474,7 +474,7 @@ jobs:
474474
strategy:
475475
fail-fast: false
476476
matrix:
477-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
477+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
478478

479479
env:
480480
AUSTIN_TESTS_PYTHON_VERSIONS: ${{ matrix.python-version }}

ChangeLog

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
2024-xx-xx v3.6.1
1+
2024-xx-xx v3.7.0
2+
3+
Added support for CPython 3.13.
24

35
Improve support for Python processes running in containers.
46

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ folder in either the SVG, PDF or PNG format
591591

592592
# Compatibility
593593

594-
Austin supports Python 3.8 through 3.12, and has been tested on the following
594+
Austin supports Python 3.8 through 3.13, and has been tested on the following
595595
platforms and architectures
596596

597597
| | <img src="art/tux.svg" /> | <img src="art/win.svg"/> | <img src="art/apple.svg"/> |

configure.ac

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ AC_PREREQ([2.69])
66
# from scripts.utils import get_current_version_from_changelog as version
77
# print(f"AC_INIT([austin], [{version()}], [https://github.com/p403n1x87/austin/issues])")
88
# ]]]
9-
AC_INIT([austin], [3.6.1], [https://github.com/p403n1x87/austin/issues])
9+
AC_INIT([austin], [3.7.0], [https://github.com/p403n1x87/austin/issues])
1010
# [[[end]]]
1111
AC_CONFIG_SRCDIR([config.h.in])
1212
AC_CONFIG_HEADERS([config.h])

doc/cheatsheet.svg

+2-2
Loading

scripts/build-wheel.py

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"Programming Language :: Python :: 3.10",
2222
"Programming Language :: Python :: 3.11",
2323
"Programming Language :: Python :: 3.12",
24+
"Programming Language :: Python :: 3.13",
2425
],
2526
"Project-URL": [
2627
"Homepage, https://github.com/P403n1x87/austin",

snap/snapcraft.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ base: core20
44
# from scripts.utils import get_current_version_from_changelog as version
55
# print(f"version: '{version()}+git'")
66
# ]]]
7-
version: '3.6.1+git'
7+
version: '3.7.0+git'
88
# [[[end]]]
99
summary: A Python frame stack sampler for CPython
1010
description: |

src/austin.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from scripts.utils import get_current_version_from_changelog as version
3535
print(f'#define VERSION "{version()}"')
3636
]]] */
37-
#define VERSION "3.6.1"
37+
#define VERSION "3.7.0"
3838
// [[[end]]]
3939

4040
#endif

src/py_proc.c

+88-21
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,30 @@ _py_proc__infer_python_version(py_proc_t * self) {
235235

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

238+
// Starting with Python 3.13 we can use the PyRuntime structure
239+
if (isvalid(self->symbols[DYNSYM_RUNTIME])) {
240+
_Py_DebugOffsets py_d;
241+
if (fail(py_proc__get_type(self, self->symbols[DYNSYM_RUNTIME], py_d))) {
242+
log_e("Cannot copy PyRuntimeState structure from remote address");
243+
FAIL;
244+
}
245+
246+
if (0 == memcmp(py_d.v3_13.cookie, _Py_Debug_Cookie, sizeof(py_d.v3_13.cookie))) {
247+
uint64_t version = py_d.v3_13.version;
248+
major = (version>>24) & 0xFF;
249+
minor = (version>>16) & 0xFF;
250+
patch = (version>>8) & 0xFF;
251+
252+
log_d("Python version (from debug offsets): %d.%d.%d", major, minor, patch);
253+
254+
self->py_v = get_version_descriptor(major, minor, patch);
255+
256+
init_version_descriptor(self->py_v, &py_d);
257+
258+
SUCCESS;
259+
}
260+
}
261+
238262
// Starting with Python 3.11 we can rely on the Py_Version symbol
239263
if (isvalid(self->symbols[DYNSYM_HEX_VERSION])) {
240264
unsigned long py_version = 0;
@@ -322,17 +346,14 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
322346

323347
V_DESC(self->py_v);
324348

325-
PyInterpreterState is;
326-
PyThreadState tstate_head;
327-
328-
if (py_proc__get_type(self, raddr, is)) {
349+
if (py_proc__copy_v(self, is, raddr, self->is)) {
329350
log_ie("Cannot get remote interpreter state");
330351
FAIL;
331352
}
353+
log_d("Interpreter state buffer %p", self->is);
354+
void * tstate_head_addr = V_FIELD_PTR(void *, self->is, py_is, o_tstate_head);
332355

333-
void * tstate_head_addr = V_FIELD(void *, is, py_is, o_tstate_head);
334-
335-
if (fail(py_proc__get_type(self, tstate_head_addr, tstate_head))) {
356+
if (fail(py_proc__copy_v(self, thread, tstate_head_addr, self->ts))) {
336357
log_e(
337358
"Cannot copy PyThreadState head at %p from PyInterpreterState instance",
338359
tstate_head_addr
@@ -342,7 +363,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
342363

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

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

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

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

490511
void * interp_head_raddr = NULL;
491512

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

512532
for (void * current_addr = lower; current_addr <= upper; current_addr += sizeof(void *)) {
513-
if (py_proc__get_type(self, current_addr, py_runtime)) {
533+
if (py_proc__copy_v(self, runtime, current_addr, self->rs)) {
514534
log_d(
515535
"Cannot copy runtime state structure from remote address %p",
516536
current_addr
517537
);
518538
continue;
519539
}
520540

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

583+
// ----------------------------------------------------------------------------
584+
static void
585+
_py_proc__free_local_buffers(py_proc_t * self) {
586+
sfree(self->is);
587+
sfree(self->ts);
588+
sfree(self->rs);
589+
}
590+
591+
// ----------------------------------------------------------------------------
592+
#define LOCAL_ALLOC(dest, src, name) { \
593+
self->dest = calloc(1, self->py_v->py_##src.size); \
594+
if (!isvalid(self->dest)) { \
595+
log_e("Cannot allocate memory for " #name); \
596+
goto error; \
597+
} \
598+
}
599+
600+
static int
601+
_py_proc__init_local_buffers(py_proc_t * self) {
602+
if (!isvalid(self)) {
603+
set_error(EPROC);
604+
FAIL;
605+
}
606+
607+
LOCAL_ALLOC(rs, runtime, "PyRuntimeState");
608+
LOCAL_ALLOC(is, is, "PyInterpreterState");
609+
LOCAL_ALLOC(ts, thread, "PyThreadState");
610+
611+
log_d("Local buffers initialised");
612+
613+
SUCCESS;
614+
615+
error:
616+
set_error(ENOMEM);
617+
618+
_py_proc__free_local_buffers(self);
619+
620+
FAIL;
621+
}
622+
563623
// ----------------------------------------------------------------------------
564624
static int
565625
_py_proc__find_interpreter_state(py_proc_t * self) {
@@ -575,6 +635,9 @@ _py_proc__find_interpreter_state(py_proc_t * self) {
575635
if (fail(_py_proc__infer_python_version(self)))
576636
FAIL;
577637

638+
if (fail(_py_proc__init_local_buffers(self)))
639+
FAIL;
640+
578641
if (self->sym_loaded || isvalid(self->map.runtime.base)) {
579642
// Try to resolve the symbols or the runtime section, if we have them
580643

@@ -703,6 +766,11 @@ py_proc_new(int child) {
703766

704767
py_proc->child = child;
705768
py_proc->gc_state_raddr = NULL;
769+
py_proc->py_v = NULL;
770+
771+
py_proc->is = NULL;
772+
py_proc->ts = NULL;
773+
py_proc->rs = NULL;
706774

707775
_prehash_symbols();
708776

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

9531021
void * interp_head_raddr;
954-
_PyRuntimeState py_runtime;
9551022

956-
if (py_proc__get_type(self, self->symbols[DYNSYM_RUNTIME], py_runtime))
1023+
if (py_proc__copy_v(self, runtime, self->symbols[DYNSYM_RUNTIME], self->rs))
9571024
FAIL;
9581025

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

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

12011268
V_DESC(self->py_v);
12021269

1203-
PyInterpreterState is;
1204-
12051270
do {
1206-
if (fail(py_proc__get_type(self, current_interp, is))) {
1271+
if (fail(py_proc__copy_v(self, is, current_interp, self->is))) {
12071272
log_ie("Failed to get interpreter state while sampling");
12081273
FAIL;
12091274
}
12101275

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

1226-
int result = _py_proc__sample_interpreter(self, &is, time_delta);
1291+
int result = _py_proc__sample_interpreter(self, self->is, time_delta);
12271292

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

12351300
if (fail(result))
12361301
FAIL;
1237-
} while (isvalid(current_interp = V_FIELD(void *, is, py_is, o_next)));
1302+
} while (isvalid(current_interp = V_FIELD_PTR(void *, self->is, py_is, o_next)));
12381303

12391304
#ifdef NATIVE
12401305
self->timestamp = gettime();
@@ -1321,6 +1386,8 @@ py_proc__destroy(py_proc_t * self) {
13211386
hash_table__destroy(self->base_table);
13221387
#endif
13231388

1389+
_py_proc__free_local_buffers(self);
1390+
13241391
sfree(self->bin_path);
13251392
sfree(self->lib_path);
13261393
sfree(self->extra);

src/py_proc.h

+18
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ typedef struct {
9999
hash_table_t * base_table;
100100
#endif
101101

102+
// Local buffers
103+
_PyRuntimeState * rs;
104+
PyInterpreterState * is;
105+
PyThreadState * ts;
106+
102107
// Platform-dependent fields
103108
proc_extra_info * extra;
104109
} py_proc_t;
@@ -208,6 +213,19 @@ py_proc__sample(py_proc_t *);
208213
*/
209214
#define py_proc__get_type(self, raddr, dt) (py_proc__memcpy(self, raddr, sizeof(dt), &dt))
210215

216+
/**
217+
* Make a local copy of a remote structure.
218+
*
219+
* @param self the process object.
220+
* @param type the type of the structure.
221+
* @param raddr the remote address of the structure.
222+
* @param dest the destination address.
223+
*
224+
* @return 0 on success.
225+
*/
226+
#define py_proc__copy_v(self, type, raddr, dest) (py_proc__memcpy(self, raddr, py_v->py_##type.size, dest))
227+
228+
211229
/**
212230
* Log the Python interpreter version
213231
* @param self the process object.

src/py_proc_list.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ py_proc_list__sample(py_proc_list_t * self) {
176176
for (py_proc_item_t * item = self->first; item != NULL; /* item = item->next */) {
177177
log_t("Sampling process with PID %d", item->py_proc->pid);
178178
stopwatch_start();
179-
if (fail(py_proc__sample(item->py_proc))) {
179+
if (!isvalid(item->py_proc->py_v) || fail(py_proc__sample(item->py_proc))) {
180180
py_proc__wait(item->py_proc);
181181
py_proc_item_t * next = item->next;
182182
_py_proc_list__remove(self, item);

0 commit comments

Comments
 (0)