diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8ac1a426..9aba86da 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -55,7 +55,7 @@ jobs:
export PATH="/c/Program Files (x86)/`ls /c/Program\ Files\ \(x86\) | grep \"[wW]i[xX] [tT]oolset\"`/bin:$PATH"
export VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p")
- gcc -s -Wall -O3 -Os -o src/austin src/*.c -lpsapi -lntdll
+ gcc -static -s -Wall -O3 -Os -o src/austin src/*.c -lpsapi -lntdll
git checkout "packaging/msi"
git checkout master
diff --git a/ChangeLog b/ChangeLog
index 60e986e7..159b17ef 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+2021-08-18 v3.1.0
+
+ Added garbage collection state sampling for Python 3.7 onward.
+
+ Bugfix: the MinGW libwinpthread library is now linked statically on Windows.
+
+
2021-06-13 v3.0.0
Added pipe mode.
diff --git a/README.md b/README.md
index 34fd7844..4a669d53 100644
--- a/README.md
+++ b/README.md
@@ -286,6 +286,7 @@ Austin -- A frame stack sampler for Python.
-e, --exclude-empty Do not output samples of threads with no frame
stacks.
-f, --full Produce the full set of metrics (time +mem -mem).
+ -g, --gc Sample the garbage collector state.
-i, --interval=n_us Sampling interval in microseconds (default is
100). Accepted units: s, ms, us.
-m, --memory Profile memory usage.
@@ -367,6 +368,17 @@ Austin can be told to profile multi-process applications with the `-C` or
process.
+## Garbage Collector Sampling
+
+Austin can sample the Python garbage collector state for application running
+with Python 3.7 and later versions. If the `-g`/`--gc` option is passed, Austin
+will append `:GC:` at the end of each collected frame stack whenver the
+garbage collector is in the collecting state. This gives you a measure of how
+*busy* the Python GC is during a run.
+
+*Since Austin 3.1.0*.
+
+
## Logging
Austin uses `syslog` on Linux and macOS, and `%TEMP%\austin.log` on Windows
diff --git a/configure.ac b/configure.ac
index e94f22ee..26e44bee 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,7 +2,7 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.69])
-AC_INIT([austin], [3.0.0], [https://github.com/p403n1x87/austin/issues])
+AC_INIT([austin], [3.1.0], [https://github.com/p403n1x87/austin/issues])
AC_CONFIG_SRCDIR([config.h.in])
AC_CONFIG_HEADERS([config.h])
AM_INIT_AUTOMAKE
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index a8b83c86..c896ec23 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,5 +1,5 @@
name: austin
-version: '3.0.0+git'
+version: '3.1.0+git'
summary: A Python frame stack sampler for CPython
description: |
Austin is a Python frame stack sampler for CPython written in pure C. It
diff --git a/src/argparse.c b/src/argparse.c
index 0524c776..24fcb5eb 100644
--- a/src/argparse.c
+++ b/src/argparse.c
@@ -52,6 +52,7 @@ parsed_args_t pargs = {
/* children */ 0,
/* exposure */ 0,
/* pipe */ 0,
+ /* gc */ 0,
};
static int exec_arg = 0;
@@ -217,6 +218,10 @@ static struct argp_option options[] = {
"pipe", 'P', NULL, 0,
"Pipe mode. Use when piping Austin output."
},
+ {
+ "gc", 'g', NULL, 0,
+ "Sample the garbage collector state."
+ },
#ifndef PL_LINUX
{
"help", '?', NULL
@@ -322,6 +327,10 @@ parse_opt (int key, char *arg, struct argp_state *state)
pargs.pipe = 1;
break;
+ case 'g':
+ pargs.gc = 1;
+ break;
+
case ARGP_KEY_ARG:
case ARGP_KEY_END:
if (pargs.attach_pid != 0 && exec_arg != 0)
@@ -464,6 +473,7 @@ static const char * help_msg = \
" -e, --exclude-empty Do not output samples of threads with no frame\n"
" stacks.\n"
" -f, --full Produce the full set of metrics (time +mem -mem).\n"
+" -g, --gc Sample the garbage collector state.\n"
" -i, --interval=n_us Sampling interval in microseconds (default is\n"
" 100). Accepted units: s, ms, us.\n"
" -m, --memory Profile memory usage.\n"
@@ -617,6 +627,10 @@ cb(const char opt, const char * arg) {
pargs.pipe = 1;
break;
+ case 'g':
+ pargs.gc = 1;
+ break;
+
case '?':
puts(help_msg);
exit(0);
diff --git a/src/argparse.h b/src/argparse.h
index 20f1b04b..d19f0c11 100644
--- a/src/argparse.h
+++ b/src/argparse.h
@@ -43,6 +43,7 @@ typedef struct {
int children;
ctime_t exposure;
int pipe;
+ int gc;
} parsed_args_t;
diff --git a/src/austin.1 b/src/austin.1
index 86d817e5..4103b0d6 100644
--- a/src/austin.1
+++ b/src/austin.1
@@ -1,7 +1,7 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.6.
-.TH AUSTIN "1" "June 2021" "austin 3.0.0" "User Commands"
+.TH AUSTIN "1" "August 2021" "austin 3.1.0" "User Commands"
.SH NAME
-austin \- manual page for austin 3.0.0
+austin \- manual page for austin 3.1.0
.SH SYNOPSIS
.B austin
[\fI\,OPTION\/\fR...] \fI\,command \/\fR[\fI\,ARG\/\fR...]
@@ -21,6 +21,9 @@ stacks.
\fB\-f\fR, \fB\-\-full\fR
Produce the full set of metrics (time +mem \fB\-mem\fR).
.TP
+\fB\-g\fR, \fB\-\-gc\fR
+Sample the garbage collector state.
+.TP
\fB\-i\fR, \fB\-\-interval\fR=\fI\,n_us\/\fR
Sampling interval in microseconds (default is
100). Accepted units: s, ms, us.
diff --git a/src/austin.c b/src/austin.c
index afb30913..65ce7e05 100644
--- a/src/austin.c
+++ b/src/austin.c
@@ -289,7 +289,11 @@ int main(int argc, char ** argv) {
austin_errno = EOK;
// Log sampling metrics
- NL;meta("duration: %lu", stats_duration());
+ NL;
+ meta("duration: %lu", stats_duration());
+ if (pargs.gc) {
+ meta("gc: %lu", _gc_time);
+ }
stats_log_metrics();NL;
diff --git a/src/austin.h b/src/austin.h
index 7381266d..ac5f7ce8 100644
--- a/src/austin.h
+++ b/src/austin.h
@@ -24,6 +24,6 @@
#define AUSTIN_H
#define PROGRAM_NAME "austin"
-#define VERSION "3.0.0"
+#define VERSION "3.1.0"
#endif
diff --git a/src/linux/py_thread.h b/src/linux/py_thread.h
index 5e9f19a6..ae96f5d1 100644
--- a/src/linux/py_thread.h
+++ b/src/linux/py_thread.h
@@ -21,6 +21,7 @@
// along with this program. If not, see .
#include
+#include
#include
#include
#include
diff --git a/src/py_proc.c b/src/py_proc.c
index bc8a21cc..e488d693 100644
--- a/src/py_proc.c
+++ b/src/py_proc.c
@@ -323,7 +323,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
if (py_proc__get_type(self, raddr, is))
return OUT_OF_BOUND;
- if (py_proc__get_type(self, is.tstate_head, tstate_head)) {
+ if (py_proc__get_type(self, V_FIELD(void *, is, py_is, o_tstate_head), tstate_head)) {
log_t(
"Cannot copy PyThreadState head at %p from PyInterpreterState instance",
is.tstate_head
@@ -347,7 +347,7 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
);
// As an extra sanity check, verify that the thread state is valid
- raddr_t thread_raddr = { .pid = PROC_REF, .addr = is.tstate_head };
+ raddr_t thread_raddr = { .pid = PROC_REF, .addr = V_FIELD(void *, is, py_is, o_tstate_head) };
py_thread_t thread;
if (fail(py_thread__fill_from_raddr(&thread, &thread_raddr, self))) {
log_d("Failed to fill thread structure");
@@ -361,6 +361,11 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
log_d("Stack trace constructed from possible interpreter state");
+ if (py_v->major == 3 && py_v->minor >= 9) {
+ self->gc_state_raddr = (void *) (((char *) raddr) + py_v->py_is.o_gc);
+ log_d("GC runtime state @ %p", self->gc_state_raddr);
+ }
+
SUCCESS;
}
@@ -475,6 +480,10 @@ _py_proc__deref_interp_head(py_proc_t * self) {
FAIL;
}
interp_head_raddr = V_FIELD(void *, py_runtime, py_runtime, o_interp_head);
+ if (py_v->major == 3 && py_v->minor < 9) {
+ self->gc_state_raddr = self->py_runtime_raddr + py_v->py_runtime.o_gc;
+ log_d("GC runtime state @ %p", self->gc_state_raddr);
+ }
}
else if (self->interp_head_raddr != NULL) {
if (py_proc__get_type(self, self->interp_head_raddr, interp_head_raddr)) {
@@ -706,7 +715,8 @@ _py_proc__run(py_proc_t * self, int try_once) {
if (
self->tstate_curr_raddr == NULL &&
self->py_runtime_raddr == NULL &&
- self->interp_head_raddr == NULL
+ self->interp_head_raddr == NULL &&
+ self->gc_state_raddr == NULL
)
log_w("No remote symbol references have been set.");
#endif
@@ -746,6 +756,7 @@ py_proc_new() {
return NULL;
py_proc->min_raddr = (void *) -1;
+ py_proc->gc_state_raddr = NULL;
// Pre-hash symbol names
if (_dynsym_hash_array[0] == 0) {
@@ -1069,6 +1080,22 @@ _py_proc__get_memory_delta(py_proc_t * self) {
}
+// ----------------------------------------------------------------------------
+int
+py_proc__is_gc_collecting(py_proc_t * self) {
+ if (!isvalid(self->gc_state_raddr))
+ return FALSE;
+
+ GCRuntimeState gc_state;
+ if (fail(py_proc__get_type(self, self->gc_state_raddr, gc_state))) {
+ log_d("Failed to get GC runtime state");
+ return -1;
+ }
+
+ return V_FIELD(int, gc_state, py_gc, o_collecting);
+}
+
+
// ----------------------------------------------------------------------------
int
py_proc__sample(py_proc_t * self) {
@@ -1080,8 +1107,9 @@ py_proc__sample(py_proc_t * self) {
if (fail(py_proc__get_type(self, self->is_raddr, is)))
FAIL;
- if (is.tstate_head != NULL) {
- raddr_t raddr = { .pid = PROC_REF, .addr = is.tstate_head };
+ void * tstate_head = V_FIELD(void *, is, py_is, o_tstate_head);
+ if (isvalid(tstate_head)) {
+ raddr_t raddr = { .pid = PROC_REF, .addr = tstate_head };
py_thread_t py_thread;
if (fail(py_thread__fill_from_raddr(&py_thread, &raddr, self)))
FAIL;
diff --git a/src/py_proc.h b/src/py_proc.h
index 4adb87a0..41761300 100644
--- a/src/py_proc.h
+++ b/src/py_proc.h
@@ -64,6 +64,7 @@ typedef struct {
void * tstate_curr_raddr;
void * py_runtime_raddr;
void * interp_head_raddr;
+ void * gc_state_raddr;
void * is_raddr;
@@ -148,6 +149,20 @@ int
py_proc__is_python(py_proc_t *);
+/**
+ * Check whether the GC is collecting for the given process.
+ *
+ * NOTE: This method makes sense only for Python>=3.7.
+ *
+ * @param py_proc_t * the process object.
+ *
+ * @return TRUE if the GC is collecting, FALSE otherwise.
+ *
+ */
+int
+py_proc__is_gc_collecting(py_proc_t *);
+
+
/**
* Sample the frame stack of each thread of the given Python process.
*
diff --git a/src/py_thread.c b/src/py_thread.c
index 220df96b..38cd4e4d 100644
--- a/src/py_thread.c
+++ b/src/py_thread.c
@@ -30,7 +30,6 @@
#include "logging.h"
#include "mem.h"
#include "platform.h"
-#include "pthread.h"
#include "timing.h"
#include "version.h"
@@ -489,6 +488,11 @@ py_thread__print_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t
}
}
+ if (pargs.gc && py_proc__is_gc_collecting(self->proc) == TRUE) {
+ fprintf(pargs.output_file, ";:GC:");
+ stats_gc_time(time_delta);
+ }
+
// Finish off sample with the metric(s)
if (pargs.full) {
fprintf(pargs.output_file, " " TIME_METRIC METRIC_SEP IDLE_METRIC METRIC_SEP MEM_METRIC "\n",
diff --git a/src/python.h b/src/python.h
index 0140c259..c40ea3ea 100644
--- a/src/python.h
+++ b/src/python.h
@@ -218,16 +218,143 @@ typedef union {
PyFrameObject3_10 v3_10;
} PyFrameObject;
+// ---- include/objimpl.h -----------------------------------------------------
+
+typedef union _gc_head3_7 {
+ struct {
+ union _gc_head3_7 *gc_next;
+ union _gc_head3_7 *gc_prev;
+ Py_ssize_t gc_refs;
+ } gc;
+ long double dummy; /* force worst-case alignment */
+} PyGC_Head3_7;
+
+typedef struct {
+ uintptr_t _gc_next;
+ uintptr_t _gc_prev;
+} PyGC_Head3_8;
+
+// ---- internal/mem.h --------------------------------------------------------
+
+#define NUM_GENERATIONS 3
+
+struct gc_generation3_7 {
+ PyGC_Head3_7 head;
+ int threshold; /* collection threshold */
+ int count; /* count of allocations or collections of younger
+ generations */
+};
+
+
+struct gc_generation3_8 {
+ PyGC_Head3_8 head;
+ int threshold; /* collection threshold */
+ int count; /* count of allocations or collections of younger
+ generations */
+};
+
+/* Running stats per generation */
+struct gc_generation_stats {
+ Py_ssize_t collections;
+ Py_ssize_t collected;
+ Py_ssize_t uncollectable;
+};
+
+struct _gc_runtime_state3_7 {
+ PyObject *trash_delete_later;
+ int trash_delete_nesting;
+ int enabled;
+ int debug;
+ struct gc_generation3_7 generations[NUM_GENERATIONS];
+ PyGC_Head3_7 *generation0;
+ struct gc_generation3_7 permanent_generation;
+ struct gc_generation_stats generation_stats[NUM_GENERATIONS];
+ int collecting;
+};
+
+struct _gc_runtime_state3_8 {
+ PyObject *trash_delete_later;
+ int trash_delete_nesting;
+ int enabled;
+ int debug;
+ struct gc_generation3_8 generations[NUM_GENERATIONS];
+ PyGC_Head3_8 *generation0;
+ struct gc_generation3_8 permanent_generation;
+ struct gc_generation_stats generation_stats[NUM_GENERATIONS];
+ int collecting;
+};
+
+typedef union {
+ struct _gc_runtime_state3_7 v3_7;
+ struct _gc_runtime_state3_8 v3_8;
+} GCRuntimeState;
+
// ---- pystate.h -------------------------------------------------------------
struct _ts; /* Forward */
-struct _is; /* Forward */
-typedef struct _is {
- struct _is *next;
+typedef struct _is2 {
+ struct _is2 *next;
struct _ts *tstate_head;
+ void* gc; /* Dummy */
+} PyInterpreterState2;
+
+// ---- internal/pycore_interp.h ----------------------------------------------
+
+typedef void *PyThread_type_lock;
+
+typedef struct _Py_atomic_int {
+ int _value;
+} _Py_atomic_int;
+
+struct _pending_calls {
+ PyThread_type_lock lock;
+ _Py_atomic_int calls_to_do;
+ int async_exc;
+#define NPENDINGCALLS 32
+ struct {
+ int (*func)(void *);
+ void *arg;
+ } calls[NPENDINGCALLS];
+ int first;
+ int last;
+};
+
+struct _ceval_state {
+ int recursion_limit;
+ int tracing_possible;
+ _Py_atomic_int eval_breaker;
+ _Py_atomic_int gil_drop_request;
+ struct _pending_calls pending;
+};
+
+typedef struct _is3_9 {
+
+ struct _is3_9 *next;
+ struct _is3_9 *tstate_head;
+
+ /* Reference to the _PyRuntime global variable. This field exists
+ to not have to pass runtime in addition to tstate to a function.
+ Get runtime from tstate: tstate->interp->runtime. */
+ struct pyruntimestate *runtime;
+
+ int64_t id;
+ int64_t id_refcount;
+ int requires_idref;
+ PyThread_type_lock id_mutex;
+
+ int finalizing;
+
+ struct _ceval_state ceval;
+ struct _gc_runtime_state3_8 gc;
+} PyInterpreterState3_9;
+
+typedef union {
+ PyInterpreterState2 v2;
+ PyInterpreterState3_9 v3_9;
} PyInterpreterState;
+
// Dummy struct _frame
struct _frame;
@@ -380,8 +507,6 @@ typedef union {
// ---- internal/pystate.h ----------------------------------------------------
-typedef void *PyThread_type_lock;
-
typedef struct pyruntimestate3_7 {
int initialized;
int core_initialized;
@@ -393,6 +518,14 @@ typedef struct pyruntimestate3_7 {
PyInterpreterState *main;
int64_t next_id;
} interpreters;
+#define NEXITFUNCS 32
+ void (*exitfuncs[NEXITFUNCS])(void);
+ int nexitfuncs;
+
+ struct _gc_runtime_state3_7 gc;
+ // struct _warnings_runtime_state warnings;
+ // struct _ceval_runtime_state ceval;
+ // struct _gilstate_runtime_state gilstate;
} _PyRuntimeState3_7;
// ---- internal/pycore_pystate.h ---------------------------------------------
@@ -410,6 +543,21 @@ typedef struct pyruntimestate3_8 {
PyInterpreterState *main;
int64_t next_id;
} interpreters;
+ // XXX Remove this field once we have a tp_* slot.
+ struct _xidregistry {
+ PyThread_type_lock mutex;
+ struct _xidregitem *head;
+ } xidregistry;
+
+ unsigned long main_thread;
+
+#define NEXITFUNCS 32
+ void (*exitfuncs[NEXITFUNCS])(void);
+ int nexitfuncs;
+
+ struct _gc_runtime_state3_8 gc;
+ // struct _ceval_runtime_state ceval;
+ // struct _gilstate_runtime_state gilstate;
} _PyRuntimeState3_8;
diff --git a/src/stats.c b/src/stats.c
index b8e2a3b3..a71f79e7 100644
--- a/src/stats.c
+++ b/src/stats.c
@@ -34,6 +34,7 @@
#include
#endif
+#include "argparse.h"
#include "error.h"
#include "logging.h"
#include "stats.h"
@@ -60,6 +61,8 @@ ctime_t _start_time;
ustat_t _error_cnt;
ustat_t _long_cnt;
+ctime_t _gc_time;
+
#if defined PL_MACOS
static clock_serv_t cclock;
#elif defined PL_WIN
@@ -162,6 +165,8 @@ stats_log_metrics(void) {
meta("errors: %ld/%ld", _error_cnt, _sample_cnt);
}
else {
+ ctime_t duration = stats_duration();
+
log_m("");
if (!_sample_cnt) {
log_m("😣 No samples collected.");
@@ -170,7 +175,14 @@ stats_log_metrics(void) {
log_m("\033[1mStatistics\033[0m");
- log_m("⌛ Sampling duration : \033[1m%.2f s\033[0m", stats_duration() / 1000000.);
+ log_m("⌛ Sampling duration : \033[1m%.2f s\033[0m", duration / 1000000.);
+
+ if (pargs.gc) {
+ log_m("🗑️ Garbage collector : \033[1m%.2f s\033[0m (\033[1m%.2f %%\033[0m)", \
+ _gc_time / 1000000., \
+ (float) _gc_time / duration * 100 \
+ );
+ }
log_m("⏱️ Frame sampling (min/avg/max) : \033[1m%lu/%lu/%lu μs\033[0m",
stats_get_min_sampling_time(),
diff --git a/src/stats.h b/src/stats.h
index 2fc514da..52ef75de 100644
--- a/src/stats.h
+++ b/src/stats.h
@@ -40,6 +40,8 @@ extern ctime_t _avg_sampling_time;
extern ustat_t _error_cnt;
extern ustat_t _long_cnt;
+
+extern ctime_t _gc_time;
#endif
@@ -92,6 +94,12 @@ stats_get_avg_sampling_time(void);
#define stats_count_error() { _error_cnt++; }
+/**
+ * Accumulate GC time.
+ */
+#define stats_gc_time(delta) { _gc_time+=(delta); }
+
+
/**
* Check the duration of the last sampling and update the statistics.
*
diff --git a/src/version.c b/src/version.c
index eef94798..8bd7b869 100644
--- a/src/version.c
+++ b/src/version.c
@@ -77,6 +77,20 @@
#define PY_RUNTIME(s) { \
sizeof(s), \
offsetof(s, interpreters.head), \
+ offsetof(s, gc), \
+}
+
+#define PY_IS(s) { \
+ sizeof(s), \
+ offsetof(s, next), \
+ offsetof(s, tstate_head), \
+ offsetof(s, gc), \
+}
+
+
+#define PY_GC(s) { \
+ sizeof(s), \
+ offsetof(s, collecting), \
}
// ---- Python 2 --------------------------------------------------------------
@@ -84,7 +98,8 @@
python_v python_v2 = {
PY_CODE (PyCodeObject2),
PY_FRAME (PyFrameObject2),
- PY_THREAD_H (PyThreadState2)
+ PY_THREAD_H (PyThreadState2),
+ PY_IS (PyInterpreterState2),
};
// ---- Python 3.3 ------------------------------------------------------------
@@ -92,7 +107,8 @@ python_v python_v2 = {
python_v python_v3_3 = {
PY_CODE (PyCodeObject3_3),
PY_FRAME (PyFrameObject2),
- PY_THREAD_H (PyThreadState2)
+ PY_THREAD_H (PyThreadState2),
+ PY_IS (PyInterpreterState2),
};
// ---- Python 3.4 ------------------------------------------------------------
@@ -100,7 +116,8 @@ python_v python_v3_3 = {
python_v python_v3_4 = {
PY_CODE (PyCodeObject3_3),
PY_FRAME (PyFrameObject2),
- PY_THREAD (PyThreadState3_4)
+ PY_THREAD (PyThreadState3_4),
+ PY_IS (PyInterpreterState2),
};
// ---- Python 3.6 ------------------------------------------------------------
@@ -108,7 +125,8 @@ python_v python_v3_4 = {
python_v python_v3_6 = {
PY_CODE (PyCodeObject3_6),
PY_FRAME (PyFrameObject2),
- PY_THREAD (PyThreadState3_4)
+ PY_THREAD (PyThreadState3_4),
+ PY_IS (PyInterpreterState2),
};
// ---- Python 3.7 ------------------------------------------------------------
@@ -117,7 +135,9 @@ python_v python_v3_7 = {
PY_CODE (PyCodeObject3_6),
PY_FRAME (PyFrameObject3_7),
PY_THREAD (PyThreadState3_7),
- PY_RUNTIME (_PyRuntimeState3_7)
+ PY_IS (PyInterpreterState2),
+ PY_RUNTIME (_PyRuntimeState3_7),
+ PY_GC (struct _gc_runtime_state3_7),
};
// ---- Python 3.8 ------------------------------------------------------------
@@ -126,16 +146,32 @@ python_v python_v3_8 = {
PY_CODE (PyCodeObject3_8),
PY_FRAME (PyFrameObject3_7),
PY_THREAD (PyThreadState3_8),
- PY_RUNTIME (_PyRuntimeState3_8)
+ PY_IS (PyInterpreterState2),
+ PY_RUNTIME (_PyRuntimeState3_8),
+ PY_GC (struct _gc_runtime_state3_8),
};
+// ---- Python 3.9 ------------------------------------------------------------
+
+python_v python_v3_9 = {
+ PY_CODE (PyCodeObject3_8),
+ PY_FRAME (PyFrameObject3_7),
+ PY_THREAD (PyThreadState3_8),
+ PY_IS (PyInterpreterState3_9),
+ PY_RUNTIME (_PyRuntimeState3_8),
+ PY_GC (struct _gc_runtime_state3_8),
+};
+
+
// ---- Python 3.10 -----------------------------------------------------------
python_v python_v3_10 = {
PY_CODE (PyCodeObject3_8),
PY_FRAME (PyFrameObject3_10),
PY_THREAD (PyThreadState3_8),
- PY_RUNTIME (_PyRuntimeState3_8)
+ PY_IS (PyInterpreterState3_9),
+ PY_RUNTIME (_PyRuntimeState3_8),
+ PY_GC (struct _gc_runtime_state3_8),
};
// ----------------------------------------------------------------------------
@@ -188,9 +224,11 @@ set_version(int version) {
// 3.7
case 7: py_v = &python_v3_7; break;
- // 3.8, 3.9
- case 8:
- case 9: py_v = &python_v3_8; break;
+ // 3.8
+ case 8: py_v = &python_v3_8; break;
+
+ //, 3.9
+ case 9: py_v = &python_v3_9; break;
// 3.10
case 10: py_v = &python_v3_10; break;
diff --git a/src/version.h b/src/version.h
index bc4eeb37..3a39801a 100644
--- a/src/version.h
+++ b/src/version.h
@@ -57,7 +57,7 @@
* @return the value of of the field of py_obj at the offset specified
* by the field argument.
*/
-#define V_FIELD(ctype, py_obj, py_type, field) (*((ctype*) (((void *) &py_obj) + py_v->py_type.field)))
+#define V_FIELD(ctype, py_obj, py_type, field) (*((ctype*) (((char *) &py_obj) + py_v->py_type.field)))
typedef unsigned long offset_t;
@@ -108,14 +108,32 @@ typedef struct {
ssize_t size;
offset_t o_interp_head;
+ offset_t o_gc;
} py_runtime_v;
+typedef struct {
+ ssize_t size;
+
+ offset_t o_next;
+ offset_t o_tstate_head;
+ offset_t o_gc;
+} py_is_v;
+
+
+typedef struct {
+ ssize_t size;
+
+ offset_t o_collecting;
+} py_gc_v;
+
typedef struct {
py_code_v py_code;
py_frame_v py_frame;
py_thread_v py_thread;
+ py_is_v py_is;
py_runtime_v py_runtime;
+ py_gc_v py_gc;
int major;
int minor;
diff --git a/test/common.bash b/test/common.bash
index 324e9abc..1adae87d 100644
--- a/test/common.bash
+++ b/test/common.bash
@@ -144,6 +144,40 @@ function assert_output {
# -----------------------------------------------------------------------------
+function assert_output_min_occurrences {
+ local count="${1}"
+ local pattern="${2}"
+ : "${output?}"
+
+ occurrences=`echo "$output" | grep "${pattern}" | wc -l`
+ if [[ $occurrences < $count ]]
+ then
+ log " Assertion failed: Not enough occurrences of pattern '${pattern}' (E: ${count} | G: ${occurrences})"
+ check_ignored
+ fi
+
+ true
+}
+
+# -----------------------------------------------------------------------------
+
+function assert_output_max_occurrences {
+ local count="${1}"
+ local pattern="${2}"
+ : "${output?}"
+
+ occurrences=`echo "$output" | grep "${pattern}" | wc -l`
+ if [[ $occurrences > $count ]]
+ then
+ log " Assertion failed: Too many occurrences of pattern '${pattern}' (E: ${count} | G: ${occurrences})"
+ check_ignored
+ fi
+
+ true
+}
+
+# -----------------------------------------------------------------------------
+
function assert_not_output {
local pattern="${1}"
: "${output?}"
diff --git a/test/sleepy.py b/test/sleepy.py
index b8e30de6..32b040b5 100644
--- a/test/sleepy.py
+++ b/test/sleepy.py
@@ -32,4 +32,4 @@ def cpu_bound():
if __name__ == "__main__":
for n in range(2):
cpu_bound()
- time.sleep(.7)
+ time.sleep(0.7)
diff --git a/test/target.py b/test/target_gc.py
similarity index 81%
rename from test/target.py
rename to test/target_gc.py
index d5f0c287..0f478e7a 100644
--- a/test/target.py
+++ b/test/target_gc.py
@@ -22,13 +22,23 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import threading
+import gc
+import os
-def keep_cpu_busy():
+if os.getenv("GC_DISABLED"):
+ gc.disable()
+
+
+class Foo:
+ def __init__(self, n):
+ self.n = n
+
+
+def keep_gc_busy():
a = []
- for i in range(60_000_000):
- a.append(i)
+ for i in range(2_000_000):
+ a.append(Foo(i))
+
if __name__ == "__main__":
- threading.Thread(target=keep_cpu_busy).start()
- keep_cpu_busy()
+ keep_gc_busy()
diff --git a/test/test_gc.bats b/test/test_gc.bats
new file mode 100644
index 00000000..bd774189
--- /dev/null
+++ b/test/test_gc.bats
@@ -0,0 +1,86 @@
+# This file is part of "austin" which is released under GPL.
+#
+# See file LICENCE or go to http://www.gnu.org/licenses/ for full license
+# details.
+#
+# Austin is a Python frame stack sampler for CPython.
+#
+# Copyright (c) 2019 Gabriele N. Tornetta .
+# All rights reserved.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+load "common"
+
+
+function invoke_austin {
+ local version="${1}"
+
+ check_python $version
+
+ log "GC State Sampling [Python $version]"
+
+ # -------------------------------------------------------------------------
+ step "Standard profiling"
+ # -------------------------------------------------------------------------
+ run $AUSTIN -i 10ms -t 1s $PYTHON test/target_gc.py
+
+ assert_success
+ assert_not_output ":GC:"
+
+ # -------------------------------------------------------------------------
+ step "GC Sampling"
+ # -------------------------------------------------------------------------
+ run $AUSTIN -i 10ms -t 1s -g $PYTHON test/target_gc.py
+
+ assert_success
+ assert_output_min_occurrences 10 ":GC:"
+
+ # -------------------------------------------------------------------------
+ step "GC Sampling :: GC disabled"
+ # -------------------------------------------------------------------------
+ export GC_DISABLED=1
+ run $AUSTIN -i 10ms -t 1s -g $PYTHON test/target_gc.py
+ unset GC_DISABLED
+
+ assert_success
+ assert_output_max_occurrences 5 ":GC:"
+
+}
+
+# -----------------------------------------------------------------------------
+
+function teardown {
+ if [ -f /tmp/austin_out.txt ]; then rm /tmp/austin_out.txt; fi
+}
+
+
+# -----------------------------------------------------------------------------
+# -- Test Cases
+# -----------------------------------------------------------------------------
+
+@test "Test GC Sampling with Python 3.7" {
+ repeat 3 invoke_austin "3.7"
+}
+
+@test "Test GC Sampling with Python 3.8" {
+ repeat 3 invoke_austin "3.8"
+}
+
+@test "Test GC Sampling with Python 3.9" {
+ repeat 3 invoke_austin "3.9"
+}
+
+@test "Test GC Sampling with Python 3.10" {
+ repeat 3 invoke_austin "3.10"
+}