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" +}