Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sub-interpreters support #198

Merged
merged 1 commit into from
Sep 11, 2023
Merged
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: 2 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
2023-xx-xx v3.6.0

Added support for sub-interpreters.

Dropped support for Python 2, 3.3, 3.4, 3.5, 3.6 and 3.7.

Bugfix: ensure that threads are resumed by austinp when an error occurs during
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,11 +367,12 @@ for some further processing.
By default, each line has the following structure:

~~~
P<pid>;T<tid>[;[frame]]* [metric]*
P<pid>;T<iid>:<tid>[;[frame]]* [metric]*
~~~

where the structure of `[frame]` and the number and type of metrics on each line
depend on the mode.
depend on the mode. The `<pid>`, `<iid>` and `<tid>` component represent the
process ID, the sub-interpreter ID, and the thread ID respectively.


## Environment variables
Expand Down Expand Up @@ -449,6 +450,15 @@ Austin can be told to profile multi-process applications with the `-C` or
process.


## Sub-interpreters

Austin has support for Python applications that make use of sub-interpreters.
This means that Austin will sample all the sub-interpreters that are running
within each process making up the Python application.

*Since Austin 3.6.0*.


## Garbage Collector Sampling

Austin can sample the Python garbage collector state for applications running
Expand Down
2 changes: 1 addition & 1 deletion scripts/requirements-bm.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
austin-python~=1.4.1
austin-python~=1.6
scipy~=1.10.1
2 changes: 1 addition & 1 deletion scripts/requirements-val.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
austin-python~=1.5
austin-python~=1.6
numpy
scipy
8 changes: 4 additions & 4 deletions src/argparse.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ const char SAMPLE_FORMAT_KERNEL[] = ";kernel:%s:0";
const char SAMPLE_FORMAT_WHERE_KERNEL[]= " \033[38;5;159m%s\033[0m 🐧\n";
#endif
#if defined PL_WIN
const char HEAD_FORMAT_DEFAULT[] = "P%I64d;T%I64x";
const char HEAD_FORMAT_WHERE[] = "\n\n%3$s%4$s Process \033[35;1m%1$I64d\033[0m 🧵 Thread \033[34;1m%2$I64d\033[0m\n\n";
const char HEAD_FORMAT_DEFAULT[] = "P%I64d;T%I64x:%I64x";
const char HEAD_FORMAT_WHERE[] = "\n\n%4$s%5$s Process \033[35;1m%1$I64d\033[0m 🧵 Thread \033[34;1m%2$I64d:%3$I64d\033[0m\n\n";
#else
const char HEAD_FORMAT_DEFAULT[] = "P%d;T%ld";
const char HEAD_FORMAT_WHERE[] = "\n\n%3$s%4$s Process \033[35;1m%1$d\033[0m 🧵 Thread \033[34;1m%2$ld\033[0m\n\n";
const char HEAD_FORMAT_DEFAULT[] = "P%d;T%ld:%ld";
const char HEAD_FORMAT_WHERE[] = "\n\n%4$s%5$s Process \033[35;1m%1$d\033[0m 🧵 Thread \033[34;1m%2$ld:%3$ld\033[0m\n\n";
#endif


Expand Down
14 changes: 7 additions & 7 deletions src/events.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,13 @@
} \
}

#define emit_stack(format, pid, tid, ...) \
{ \
if (pargs.binary) { \
mojo_stack(pid, tid); \
} else { \
fprintfp(pargs.output_file, format, pid, tid, __VA_ARGS__); \
} \
#define emit_stack(format, pid, iid, tid, ...) \
{ \
if (pargs.binary) { \
mojo_stack(pid, iid, tid); \
} else { \
fprintfp(pargs.output_file, format, pid, iid, tid, __VA_ARGS__); \
} \
}

#define emit_frame_ref(format, frame) \
Expand Down
9 changes: 5 additions & 4 deletions src/mojo.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#include "cache.h"
#include "platform.h"

#define MOJO_VERSION 2
#define MOJO_VERSION 3

enum {
MOJO_RESERVED,
Expand Down Expand Up @@ -119,9 +119,10 @@ static inline void mojo_integer(mojo_int_t integer, int sign) {
mojo_string(label); \
mojo_fstring(__VA_ARGS__);

#define mojo_stack(pid, tid) \
mojo_event(MOJO_STACK); \
mojo_integer(pid, 0); \
#define mojo_stack(pid, iid, tid) \
mojo_event(MOJO_STACK); \
mojo_integer(pid, 0); \
mojo_integer(iid, 0); \
mojo_fstring(FORMAT_TID, tid);

#define mojo_frame(frame) \
Expand Down
61 changes: 36 additions & 25 deletions src/py_proc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,7 @@ _py_proc__sample_interpreter(py_proc_t * self, PyInterpreterState * is, ctime_t
current_thread = _py_proc__get_current_thread_state_raddr(self);
}

int64_t interp_id = V_FIELD_PTR(int64_t, is, py_is, o_id);
do {
if (pargs.memory) {
mem_delta = 0;
Expand All @@ -1183,6 +1184,7 @@ _py_proc__sample_interpreter(py_proc_t * self, PyInterpreterState * is, ctime_t

py_thread__emit_collapsed_stack(
&py_thread,
interp_id,
time_delta,
mem_delta
);
Expand All @@ -1200,45 +1202,54 @@ _py_proc__sample_interpreter(py_proc_t * self, PyInterpreterState * is, ctime_t
// ----------------------------------------------------------------------------
int
py_proc__sample(py_proc_t * self) {
ctime_t time_delta = gettime() - self->timestamp; // Time delta since last sample.
ctime_t time_delta = gettime() - self->timestamp; // Time delta since last sample.
void * current_interp = self->is_raddr;

V_DESC(self->py_v);

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

void * tstate_head = V_FIELD(void *, 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.
SUCCESS;
do {
if (fail(py_proc__get_type(self, current_interp, is))) {
log_ie("Failed to get interpreter state while sampling");
FAIL;
}

#ifdef NATIVE
raddr_t raddr = { .pref = self->proc_ref, .addr = tstate_head };
if (fail(_py_proc__interrupt_threads(self, &raddr))) {
log_ie("Failed to interrupt threads");
FAIL;
}
time_delta = gettime() - self->timestamp;
#endif
void * tstate_head = V_FIELD(void *, 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.
SUCCESS;

int result = _py_proc__sample_interpreter(self, &is, time_delta);
#ifdef NATIVE
raddr_t raddr = { .pref = self->proc_ref, .addr = tstate_head };
if (fail(_py_proc__interrupt_threads(self, &raddr))) {
log_ie("Failed to interrupt threads");
FAIL;
}
time_delta = gettime() - self->timestamp;
#endif

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

#ifdef NATIVE
if (fail(_py_proc__resume_threads(self, &raddr))) {
log_ie("Failed to resume threads");
FAIL;
}
#endif

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

#ifdef NATIVE
self->timestamp = gettime();

if (fail(_py_proc__resume_threads(self, &raddr))) {
log_ie("Failed to resume threads");
FAIL;
}
#else
self->timestamp += time_delta;
#endif

return result;
SUCCESS;
} /* py_proc__sample */


Expand Down
4 changes: 2 additions & 2 deletions src/py_thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ py_thread__next(py_thread_t * self) {

// ----------------------------------------------------------------------------
void
py_thread__emit_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t mem_delta) {
py_thread__emit_collapsed_stack(py_thread_t * self, int64_t interp_id, ctime_t time_delta, ssize_t mem_delta) {
if (!pargs.full && pargs.memory && mem_delta == 0)
return;

Expand Down Expand Up @@ -924,7 +924,7 @@ py_thread__emit_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t

// Group entries by thread.
emit_stack(
pargs.head_format, self->proc->pid, self->tid,
pargs.head_format, self->proc->pid, interp_id, self->tid,
// These are relevant only in `where` mode
is_idle ? "💤" : "🚀",
self->proc->child ? "🧒" : ""
Expand Down
3 changes: 2 additions & 1 deletion src/py_thread.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,12 @@ py_thread__next(py_thread_t *);
* Print the frame stack using the collapsed format.
*
* @param py_thread_t self.
* @param int64_t the interpreter ID.
* @param ctime_t the time delta.
* @param ssize_t the memory delta.
*/
void
py_thread__emit_collapsed_stack(py_thread_t *, ctime_t, ssize_t);
py_thread__emit_collapsed_stack(py_thread_t *, int64_t, ctime_t, ssize_t);


/**
Expand Down
1 change: 1 addition & 0 deletions src/python/interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ struct _ts; /* Forward */
typedef struct _is2 {
struct _is2 *next;
struct _ts *tstate_head;
int64_t id;
void* gc; /* Dummy */
} PyInterpreterState2;

Expand Down
4 changes: 4 additions & 0 deletions src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ typedef struct {

offset_t o_next;
offset_t o_tstate_head;
offset_t o_id;
offset_t o_gc;
offset_t o_gil_state;
} py_is_v;
Expand Down Expand Up @@ -303,20 +304,23 @@ typedef struct {
sizeof(s), \
offsetof(s, next), \
offsetof(s, tstate_head), \
offsetof(s, id), \
offsetof(s, gc), \
}

#define PY_IS_311(s) { \
sizeof(s), \
offsetof(s, next), \
offsetof(s, threads.head), \
offsetof(s, id), \
offsetof(s, gc), \
}

#define PY_IS_312(s) { \
sizeof(s), \
offsetof(s, next), \
offsetof(s, threads.head), \
offsetof(s, id), \
offsetof(s, gc), \
offsetof(s, ceval.gil), \
}
Expand Down
1 change: 1 addition & 0 deletions test/functional/test_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def test_fork_wall_time(austin, py, heap, mojo):
assert len(processes(result.stdout)) == 1, compress(result.stdout)
ts = threads(result.stdout)
assert len(ts) == 2, compress(result.stdout)
assert all(len(t[1].split(":")) == 2 for t in ts), "threads have interpreter ID"

assert has_pattern(result.stdout, "target34.py:keep_cpu_busy:3"), compress(
result.stdout
Expand Down
2 changes: 1 addition & 1 deletion test/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
austin-python~=1.5
austin-python~=1.6
flaky
pytest
pytest-xdist
Expand Down
Loading