@@ -135,6 +143,15 @@ by Peter Norton for a general overview of Austin.
Keep reading for more tool ideas and examples!
+---
+
+💜Austin is a free and open-source project. A lot of
+effort goes into its development to ensure the best performance and that it
+stays up-to-date with the latest Python releases. If you find it useful,
+consider sponsoring this
+project.🙏
+
+---
# Installation
@@ -156,7 +173,8 @@ On macOS, Austin can be easily installed from the command line using [Homebrew].
Anaconda users can install Austin from [Conda Forge].
For any other platform, compiling Austin from sources is as easy as cloning the
-repository and running the C compiler.
+repository and running the C compiler. The [Releases][releases] page has many
+pre-compiled binaries that are ready to be uncompressed and used.
## With `autotools`
@@ -282,7 +300,9 @@ bug with Austin and you want to report it here.
~~~
Usage: austin [OPTION...] command [ARG...]
-Austin -- A frame stack sampler for Python.
+Austin is a frame stack sampler for CPython that is used to extract profiling
+data out of a running Python process (and all its children, if required) that
+requires no instrumentation and has practically no impact on the tracee.
-a, --alt-format Alternative collapsed stack sample format.
-C, --children Attach to child processes.
@@ -291,17 +311,18 @@ Austin -- A frame stack sampler for Python.
-f, --full Produce the full set of metrics (time +mem -mem).
-g, --gc Sample the garbage collector state.
-h, --heap=n_mb Maximum heap size to allocate to increase sampling
- accuracy, in MB (default is 256).
+ accuracy, in MB (default is 0).
-i, --interval=n_us Sampling interval in microseconds (default is
100). Accepted units: s, ms, us.
-m, --memory Profile memory usage.
-o, --output=FILE Specify an output file for the collected samples.
- -p, --pid=PID The the ID of the process to which Austin should
- attach.
+ -p, --pid=PID Attach to the process with the given PID.
-P, --pipe Pipe mode. Use when piping Austin output.
-s, --sleepless Suppress idle samples to estimate CPU time.
-t, --timeout=n_ms Start up wait time in milliseconds (default is
100). Accepted units: s, ms.
+ -w, --where=PID Dump the stacks of all the threads within the
+ process with the given PID.
-x, --exposure=n_sec Sample for n_sec seconds only.
-?, --help Give this help list
--usage Give a short usage message
@@ -384,20 +405,48 @@ garbage collector is in the collecting state. This gives you a measure of how
*Since Austin 3.1.0*.
+## Where?
+
+If you are only interested in what is currently happening inside a Python
+process, you can have a quick overview printed on the terminal with the
+`-w/--where` option. This takes the PID of the process whose threads you want to
+inspect, e.g.
+
+~~~ console
+sudo austin -w `pgrep -f my-running-python-app`
+~~~
+
+Below is an example of what the output looks like
+
+
+
+
+
+This works with the `-C/--children` option too. The emojis to the left indicate
+whether the thread is active or sleeping and whether the process is a child or
+not.
+
+*Since Austin 3.3.0*.
+
+
## Sampling Accuracy
Austin tries to keep perturbations to the tracee at a minimum. In order to do
-so, the tracee is never halted. To improve sampling accuracy, Austin allocates a
-heap that is used to get large snapshots of the private VM of the tracee that is
-likely to contain frame information in a single attempt. The larger the heap is
-allowed the grow, the more accurate the results. The maximum size of the heap
-that Austin is allowed to allocate can be controlled with the `-h/--heap`
-option, followed by the maximum size in bytes. By default Austin allocates a
-maximum of 256 MB. On systems with low resource limits, it is advisable to
-reduce this value.
+so, the tracee is never halted. To improve sampling accuracy, Austin can
+allocate a heap that is used to get large snapshots of the private VM of the
+tracee that is likely to contain frame information in a single attempt. The
+larger the heap is allowed the grow, the more accurate the results. The maximum
+size of the heap that Austin is allowed to allocate can be controlled with the
+`-h/--heap` option, followed by the maximum size in bytes. By default Austin
+does not allocate a heap, which is ideal on systems with limited resources. If
+you think your results are not accurate, try setting this parameter.
*Since Austin 3.2.0*.
+*Changed in Austin 3.3.0*: the default heap size is 0.
+
## Native Frame Stack
@@ -411,13 +460,13 @@ sources make sure that you have the development version of the `libunwind`
library available on your system, for example on Ubuntu,
~~~ console
-sudo apt install libunwind-dev
+sudo apt install libunwind-dev binutils-dev
~~~
and compile with
~~~ console
-gcc -O3 -Os -Wall -pthread src/*.c -DAUSTINP -lunwind-ptrace -lunwind-generic -o src/austinp
+gcc -O3 -Os -Wall -pthread src/*.c -DAUSTINP -lunwind-ptrace -lunwind-generic -lbfd -o src/austinp
~~~
then use as per normal. The extra `-k/--kernel` option is available with
@@ -440,6 +489,21 @@ python3 utils/resolve.py mysamples.austin > mysamples_resolved.austin
Internally, the script uses `addr2line(1)` to determine source and line number
given an address, when possible.
+> Whilst `austinp` comes with a stripped-down implementation of `addr2line`, it
+> is only used for the "where" option, as resolving symbols at runtime is
+> expensive. This is to minimise the impact of austinp on the tracee, increase
+> accuracy and maximise the sampling rate.
+
+The [where](#where) option is also available for the `austinp` variant and will
+show both native and Python frames. Highlighting helps tell frames apart. The
+`-k` options outputs Linux kernel frames too, as shown in this example
+
+
+
+
+
## Logging
@@ -453,11 +517,11 @@ error rates below 1% on average.
## Cheat sheet
All the above Austin options and arguments are summarised in a cheat sheet that
-you can find in the [art](https://github.com/P403n1x87/austin/blob/master/art/)
+you can find in the [doc](https://github.com/P403n1x87/austin/blob/master/doc/)
folder in either the SVG, PDF or PNG format
-
@@ -466,12 +530,12 @@ folder in either the SVG, PDF or PNG format
Austin supports Python 2.3-2.7 and 3.3-3.10 and has been tested on the
following platforms and architectures
-|| * | ** | *** |
-|--- |---|---|---|
-| **x86_64** | ✓ | ✓ | ✓ |
-| **i686** | ✓ | | ✓ |
-| **arm64** | ✓ | | |
-| **ppc64le** | ✓ | | |
+| | * | ** | *** |
+| ----------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
+| **x86_64** | ✓ | ✓ | ✓ |
+| **i686** | ✓ | | ✓ |
+| **arm64** | ✓ | | |
+| **ppc64le** | ✓ | | |
\* In order to attach to an external process, Austin requires the CAP_SYS_PTRACE
capability. This means that you will have to either use ``sudo`` when attaching
@@ -496,8 +560,10 @@ Python process.
Capitan, Austin cannot profile Python processes that use an executable located
in the `/bin` folder, even with `sudo`. Hence, either run the interpreter from a
virtual environment or use a Python interpreter that is installed in, e.g.,
-`/Applications` or via `brew` with the default prefix (`/usr/local`). Even in
-these cases, though, the use of `sudo` is required.
+`/Applications` or via alternative methods, like `brew` with the default prefix
+(`/usr/local`), or [pyenv][pyenv]. Even in these cases, though, the use of
+`sudo` is required. Austin is unlikely to work with interpreters installed using
+the official installers from [python.org](https://python.org).
> **NOTE** Austin *might* work with other versions of Python on all the
> platforms and architectures above. So it is worth giving it a try even if
@@ -608,7 +674,7 @@ TUI to work.
> The TUI is based on `python-curses`. The version included with the standard
> Windows installations of Python is broken so it won't work out of the box. A
-> solution is to install the the wheel of the port to Windows from
+> solution is to install the wheel of the port to Windows from
> [this](https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses) page. Wheel files
> can be installed directly with `pip`, as described in the
> [linked](https://pip.pypa.io/en/latest/user_guide/#installing-from-wheels)
@@ -741,6 +807,8 @@ by chipping in a few pennies on [PayPal.Me](https://www.paypal.me/gtornetta/1).
[Homebrew]: https://formulae.brew.sh/formula/austin
[latest release]: https://github.com/P403n1x87/austin/releases/latest
[pprof]: https://github.com/google/pprof
+[pyenv]: https://github.com/pyenv/pyenv
+[releases]: https://github.com/P403n1x87/austin/releases
[Scoop]: https://scoop.sh/
[Speedscope]: https://speedscope.app
[Visual Studio Code]: https://marketplace.visualstudio.com/items?itemName=p403n1x87.austin-vscode
diff --git a/art/austin-where-kernel.png b/art/austin-where-kernel.png
new file mode 100644
index 00000000..24d2e6af
Binary files /dev/null and b/art/austin-where-kernel.png differ
diff --git a/art/austin-where.png b/art/austin-where.png
new file mode 100644
index 00000000..adbc8026
Binary files /dev/null and b/art/austin-where.png differ
diff --git a/art/cheatsheet.png b/art/cheatsheet.png
deleted file mode 100644
index 11591fb7..00000000
Binary files a/art/cheatsheet.png and /dev/null differ
diff --git a/configure.ac b/configure.ac
index 137ebbaf..83c9c85c 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.2.0], [https://github.com/p403n1x87/austin/issues])
+AC_INIT([austin], [3.3.0], [https://github.com/p403n1x87/austin/issues])
AC_CONFIG_SRCDIR([config.h.in])
AC_CONFIG_HEADERS([config.h])
AM_INIT_AUTOMAKE
@@ -15,6 +15,35 @@ AC_PROG_CPP
AC_LANG([C])
# Checks for libraries.
+AC_CHECK_HEADER(libunwind-ptrace.h, [
+ AM_CONDITIONAL(BUILD_AUSTINP, true)
+ AUSTINP_CFLAGS="-DAUSTINP"
+ AUSTINP_LDADD="-l:libunwind-ptrace.a -l:liblzma.a -l:libunwind-generic.a -l:libunwind.a"
+ echo "including build of austinp"
+], [
+ AM_CONDITIONAL(BUILD_AUSTINP, false)
+ echo "not building austinp: missing libunwind"
+])
+AC_CHECK_LIB(bfd, bfd_openr, [
+ AC_DEFINE([HAVE_BFD], [1], ["Compile with BFD support"])
+ AUSTINP_CFLAGS+=" -DHAVE_BFD"
+ AUSTINP_LDADD+=" -lbfd"
+ echo "enabling symbol resolution support for austinp"
+], [
+ echo "austinp will be built without symbol resolution support: missing libbfd"
+])
+AC_CHECK_LIB(iberty, bfd_demangle, [
+ AC_DEFINE([HAVE_LIBERTY], [1], ["Compile with C++ name demangling support"])
+ AUSTINP_CFLAGS+=" -DHAVE_LIBERTY"
+ echo "enabling C++ name demangling support for austinp"
+], [
+ echo "austinp will be built without C++ names demangling support: missing libiberty"
+], [
+ -lbfd
+])
+
+AC_SUBST(AUSTINP_CFLAGS, [$AUSTINP_CFLAGS])
+AC_SUBST(AUSTINP_LDADD, [$AUSTINP_LDADD])
# Checks for header files.
AC_HEADER_STDC
@@ -30,6 +59,28 @@ AC_FUNC_MALLOC
AC_FUNC_REALLOC
AC_CHECK_FUNCS([strstr])
+# Coverage
+AC_ARG_ENABLE([coverage], [
+ --enable-coverage Turn on coverage], [
+case "${enableval}" in
+ yes) coverage=true ;;
+ no) coverage=false ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-coverage]) ;;
+esac], [coverage=false])
+
+AM_CONDITIONAL([COVERAGE], [test x$coverage = xtrue])
+
+# Debug symbols
+AC_ARG_ENABLE([debug-symbols], [
+ --enable-debug-symbols Include debug symbols], [
+case "${enableval}" in
+ yes) debugsymbols=true ;;
+ no) debugsymbols=false ;;
+ *) AC_MSG_ERROR([bad value ${enableval} for --enable-debug-symbols]) ;;
+esac], [debugsymbols=false])
+
+AM_CONDITIONAL([DEBUG_SYMBOLS], [test x$debugsymbols = xtrue])
+
AC_CONFIG_FILES([Makefile
src/Makefile])
AC_OUTPUT
diff --git a/art/cheatsheet.pdf b/doc/cheatsheet.pdf
similarity index 70%
rename from art/cheatsheet.pdf
rename to doc/cheatsheet.pdf
index 7b0e822c..a2b6a5bf 100644
Binary files a/art/cheatsheet.pdf and b/doc/cheatsheet.pdf differ
diff --git a/doc/cheatsheet.png b/doc/cheatsheet.png
new file mode 100644
index 00000000..cef493a0
Binary files /dev/null and b/doc/cheatsheet.png differ
diff --git a/art/cheatsheet.svg b/doc/cheatsheet.svg
similarity index 98%
rename from art/cheatsheet.svg
rename to doc/cheatsheet.svg
index 1d9adc49..d4116648 100644
--- a/art/cheatsheet.svg
+++ b/doc/cheatsheet.svg
@@ -756,9 +756,9 @@
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
- inkscape:zoom="1.979899"
- inkscape:cx="354.73748"
- inkscape:cy="1022.7744"
+ inkscape:zoom="0.9899495"
+ inkscape:cx="195.32501"
+ inkscape:cy="503.18102"
inkscape:document-units="mm"
inkscape:current-layer="layer3"
showgrid="false"
@@ -5448,17 +5448,17 @@
inkscape:label="Footer"
style="display:inline">
+ y="268.17651" />
+ transform="matrix(0.12290635,0,0,0.12290635,175.90439,258.43432)">
austin
+ style="fill:#e6f0ff;fill-opacity:1;stroke-width:0.00449889" />
+ style="fill:#e6f0ff;fill-opacity:1;stroke-width:0.05132131" />
OUTPUT
FORMAT
Mode
TOOLS
Attach to a running process
+ sodipodi:role="line">Attach to a running process*
-p 123 -i 10ms
Set start-up timeout (on slow machines)
austin -p 123 -t 1s
Wall clock time
austin python myscript.py
@@ -5940,12 +5940,12 @@
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;display:inline;fill:#404040;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="5.8901877"
- y="239.70287"
+ y="251.87354"
id="text1779">CPU time
+ y="253.74443" />
austin -s python myscript.py
Memory
austin Wall clock time and garbage collector
+ y="43.635376" />
austin -g python myscript.py
All metrics
austin All metrics and garbage collector
+ y="81.753525" />
austin -fg -p 123
Emit to STDOUT (Python STDOUT suppressed)
austin python -m mymodule
@@ -6126,12 +6126,12 @@
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;display:inline;fill:#404040;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="110.35671"
- y="119.1333"
+ y="128.65288"
id="text1855">Pipe to other tools (Python STDOUT suppressed)
@@ -6141,17 +6141,17 @@
width="95.000885"
height="6.4413304"
x="110.08945"
- y="121.00412" />
+ y="130.5237" />
austin -P ./myscript.py Default
+ y="166.36888" />
austin ./myscript.py
Alternative
austin
+ y="184.13445" />
foomodule:foo:42
@@ -6246,15 +6246,15 @@
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;display:inline;fill:#f0f0f0;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="167.71144"
- y="177.37228"
+ y="186.89177"
id="text1901">foomodule:foo:43
barmodule:bar:13
+ y="179.66463" />
+ y="175.19986" />
+ y="216.63365" />
foomodule:foo
+ y="212.22699" />
barmodule:bar
+ y="203.49104" />
L42
@@ -6378,12 +6378,12 @@
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;display:inline;fill:#f0f0f0;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="167.61176"
- y="209.8875"
+ y="219.40698"
id="text1947">L43
+ y="207.77966" />
L13
-x 3s ./myscript.py
Supported platforms
+ transform="matrix(0.00524065,0,0,0.00524065,50.105676,278.18175)">
Supported interpreters
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.88055556px;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#e6f0ff;fill-opacity:1;stroke-width:0.26458332">Supported interpreters
2.7 and 3.3 thru 3.10
+ sodipodi:role="line">2.3 thru 2.7 and 3.3 thru 3.10
CPU time and garbage collector
austin Redirect to file (Python STDOUT suppressed)
@@ -6785,35 +6785,35 @@
width="95.000885"
height="6.4413304"
x="110.08945"
- y="108.29811" />
+ y="117.81771" />
austin -p 123 > /path/to/samples.austin
Emit to file (Python STDOUT preserved)
austin -o /path/to/samples.austin -p 123
Austin TUI
Austin VS Code
+ y="253.92024" />
+ transform="matrix(0.00486693,0,0,0.00486693,190.03149,255.02957)">
code --install-extension p403n1x87.austin-vscode
@@ -7159,15 +7159,15 @@
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;display:inline;fill:#e6f0ff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
x="116.23132"
- y="272.98712"
+ y="274.57465"
id="text2398">p403n1x87/austin
+ y="274.57465"
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.17499995px;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono Bold';fill:#e6f0ff;fill-opacity:1;stroke-width:0.26458332">p403n1x87/austin
@@ -7179,12 +7179,12 @@
@AustinSampler
@@ -7194,7 +7194,7 @@
x="108.92651"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52777767px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;display:inline;fill:#e6f0ff;fill-opacity:1;stroke:none;stroke-width:0.26458332"
xml:space="preserve">https://github.com/P403n1x87/austin/issues
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.175px;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono Bold';fill:#e6f0ff;fill-opacity:1;stroke-width:0.26458332">https://github.com/P403n1x87/austin/issues
Set heap size (for accurate results)
+ y="215.01791"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.52777767px;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#404040;fill-opacity:1;stroke-width:0.26458332">Set heap size (for more accurate results)
+ y="216.88881" />
austin -h 512 python -m mymodule
+ Where?*
+
+ austin -w 123
+ * requires superuser capabilities on Linux
for version 3.2
+ sodipodi:role="line">for version 3.3
AUSTIN
Frame stack sampler for CPython
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333335px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;text-anchor:start;fill:#e6f0ff;fill-opacity:1">Frame stack sampler for CPython
\fR
+.RE
+.PP
+Attach to a process and its children
+.PP
+.RS
+# austin -Cp \fI\,\fR
+.RE
+.PP
+Where is a Python process at?
+.PP
+.RS
+# austin -w \fI\,\fR
+.RE
+.PP
+Set the sampling interval
+.PP
+.RS
+# austin -i \fI\,10ms\fR -p
+.RE
+.PP
+Save collected on-CPU samples to file
+.PP
+.RS
+$ austin -so \fI/path/to/file.austin\fR ./myscript.py
+.RE
+.PP
+Sample for 5 seconds only
+.PP
+.RS
+# austin -x \fI5\fR -p
+.RE
+
+
diff --git a/doc/genman.sh b/doc/genman.sh
new file mode 100644
index 00000000..c6b9a8fd
--- /dev/null
+++ b/doc/genman.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+help2man \
+ -n "Frame stack sampler for CPython" \
+ -i doc/examples.troff \
+ src/austin > src/austin.1
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 9052143a..4e237952 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -1,5 +1,5 @@
name: austin
-version: '3.2.0+git'
+version: '3.3.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
@@ -20,7 +20,10 @@ parts:
plugin: autotools
source: git://github.com/P403n1x87/austin
source-depth: 1
+ build-packages: [libunwind-dev, binutils-dev, libiberty-dev]
apps:
austin:
command: bin/austin
+ austinp:
+ command: bin/austinp
diff --git a/src/Makefile.am b/src/Makefile.am
index 6d8de8b7..92188e95 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -20,19 +20,47 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-AM_CFLAGS =-I$(top_srcdir)/src -Wall -O3 -Os -s -pthread
+AM_CFLAGS = -I$(srcdir) -Wall -pthread
+OPT_FLAGS = -O3
+STRIP_FLAGS = -Os -s
+
+if DEBUG_SYMBOLS
+DEBUG_OPTS = -g
+undefine STRIP_FLAGS
+endif
+
+if COVERAGE
+COVERAGE_FLAGS = -g -fprofile-arcs -ftest-coverage
+undefine OPT_FLAGS
+undefine STRIP_FLAGS
+endif
man_MANS = austin.1
+
bin_PROGRAMS = austin
+
+# ---- Austin ----
+
+austin_CFLAGS = $(AM_CFLAGS) $(OPT_FLAGS) $(STRIP_FLAGS) $(COVERAGE_FLAGS) $(DEBUG_OPTS)
austin_SOURCES = \
argparse.c \
austin.c \
- dict.c \
+ cache.c \
error.c \
logging.c \
- version.c \
stats.c \
platform.c \
py_proc_list.c \
py_proc.c \
py_thread.c
+
+
+# ---- Austin P ----
+
+if BUILD_AUSTINP
+bin_PROGRAMS += austinp
+
+austinp_SOURCES = $(austin_SOURCES)
+austinp_CFLAGS = $(austin_CFLAGS) @AUSTINP_CFLAGS@
+austinp_LDADD = @AUSTINP_LDADD@
+endif
diff --git a/src/argparse.c b/src/argparse.c
index c5eb13ad..138d68d0 100644
--- a/src/argparse.c
+++ b/src/argparse.c
@@ -39,10 +39,23 @@
#define DEFAULT_SAMPLING_INTERVAL 100
#endif
#define DEFAULT_INIT_RETRY_CNT 100
-#define DEFAULT_HEAP_SIZE 256
+#define DEFAULT_HEAP_SIZE 0
const char SAMPLE_FORMAT_NORMAL[] = ";%s:%s:%d";
const char SAMPLE_FORMAT_ALTERNATIVE[] = ";%s:%s;L%d";
+const char SAMPLE_FORMAT_WHERE[] = " \033[33;1m%2$s\033[0m (\033[36;1m%1$s\033[0m:\033[32;1m%3$d\033[0m)\n";
+#ifdef NATIVE
+const char SAMPLE_FORMAT_WHERE_NATIVE[]= " \033[38;5;246m%2$s\033[0m (\033[38;5;248;1m%1$s\033[0m:\033[38;5;246m%3$d\033[0m)\n";
+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";
+#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";
+#endif
// Globals for command line arguments
@@ -50,9 +63,15 @@ parsed_args_t pargs = {
/* t_sampling_interval */ DEFAULT_SAMPLING_INTERVAL,
/* timeout */ DEFAULT_INIT_RETRY_CNT * 1000,
/* attach_pid */ 0,
+ /* where */ 0,
/* exclude_empty */ 0,
/* sleepless */ 0,
/* format */ (char *) SAMPLE_FORMAT_NORMAL,
+ #ifdef NATIVE
+ /* native_format */ (char *) SAMPLE_FORMAT_NORMAL,
+ /* kernel_format */ (char *) SAMPLE_FORMAT_KERNEL,
+ #endif
+ /* head_format */ (char *) HEAD_FORMAT_DEFAULT,
/* full */ 0,
/* memory */ 0,
/* output_file */ NULL,
@@ -165,7 +184,10 @@ const char * argp_program_version = PROGRAM_NAME " " VERSION;
const char * argp_program_bug_address = \
"";
-static const char * doc = "Austin -- A frame stack sampler for Python.";
+static const char * doc = \
+"Austin is a frame stack sampler for CPython that is used to extract profiling "
+"data out of a running Python process (and all its children, if required) "
+"that requires no instrumentation and has practically no impact on the tracee.";
#else
@@ -212,7 +234,11 @@ static struct argp_option options[] = {
},
{
"pid", 'p', "PID", 0,
- "The the ID of the process to which Austin should attach."
+ "Attach to the process with the given PID."
+ },
+ {
+ "where", 'w', "PID", 0,
+ "Dump the stacks of all the threads within the process with the given PID."
},
{
"output", 'o', "FILE", 0,
@@ -237,7 +263,7 @@ static struct argp_option options[] = {
{
"heap", 'h', "n_mb", 0,
"Maximum heap size to allocate to increase sampling accuracy, in MB "
- "(default is 256)."
+ "(default is 0)."
},
#ifdef NATIVE
@@ -303,6 +329,9 @@ parse_opt (int key, char *arg, struct argp_state *state)
case 'a':
pargs.format = (char *) SAMPLE_FORMAT_ALTERNATIVE;
+ #ifdef NATIVE
+ pargs.native_format = pargs.format;
+ #endif
break;
case 'e':
@@ -361,6 +390,21 @@ parse_opt (int key, char *arg, struct argp_state *state)
pargs.heap > LONG_MAX
)
argp_error(state, "the heap size must be a positive integer");
+ pargs.heap <<= 20;
+ break;
+
+ case 'w':
+ if (str_to_num(arg, &l_pid) == 1 || l_pid <= 0)
+ argp_error(state, "invalid PID");
+ pargs.attach_pid = (pid_t) l_pid;
+ pargs.where = TRUE;
+
+ pargs.head_format = (char *) HEAD_FORMAT_WHERE;
+ pargs.format = (char *) SAMPLE_FORMAT_WHERE;
+ #ifdef NATIVE
+ pargs.native_format = (char *) SAMPLE_FORMAT_WHERE_NATIVE;
+ pargs.kernel_format = (char *) SAMPLE_FORMAT_WHERE_KERNEL;
+ #endif
break;
#ifdef NATIVE
@@ -504,7 +548,9 @@ _handle_opts(arg_option * opts, arg_callback cb, int * argi, int argc, char ** a
static const char * help_msg = \
"Usage: austin [OPTION...] command [ARG...]\n"
-"Austin -- A frame stack sampler for Python.\n"
+"Austin is a frame stack sampler for CPython that is used to extract profiling\n"
+"data out of a running Python process (and all its children, if required) that\n"
+"requires no instrumentation and has practically no impact on the tracee.\n"
"\n"
" -a, --alt-format Alternative collapsed stack sample format.\n"
" -C, --children Attach to child processes.\n"
@@ -513,17 +559,18 @@ static const char * help_msg = \
" -f, --full Produce the full set of metrics (time +mem -mem).\n"
" -g, --gc Sample the garbage collector state.\n"
" -h, --heap=n_mb Maximum heap size to allocate to increase sampling\n"
-" accuracy, in MB (default is 256).\n"
+" accuracy, in MB (default is 0).\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"
" -o, --output=FILE Specify an output file for the collected samples.\n"
-" -p, --pid=PID The the ID of the process to which Austin should\n"
-" attach.\n"
+" -p, --pid=PID Attach to the process with the given PID.\n"
" -P, --pipe Pipe mode. Use when piping Austin output.\n"
" -s, --sleepless Suppress idle samples to estimate CPU time.\n"
" -t, --timeout=n_ms Start up wait time in milliseconds (default is\n"
" 100). Accepted units: s, ms.\n"
+" -w, --where=PID Dump the stacks of all the threads within the\n"
+" process with the given PID.\n"
" -x, --exposure=n_sec Sample for n_sec seconds only.\n"
" -?, --help Give this help list\n"
" --usage Give a short usage message\n"
@@ -641,6 +688,19 @@ cb(const char opt, const char * arg) {
}
break;
+ case 'w':
+ if (
+ str_to_num((char *) arg, (long *) &pargs.attach_pid) == 1 ||
+ pargs.attach_pid <= 0
+ ) {
+ arg_error("invalid PID");
+ }
+ pargs.where = TRUE;
+
+ pargs.head_format = (char *) HEAD_FORMAT_WHERE;
+ pargs.format = (char *) SAMPLE_FORMAT_WHERE;
+ break;
+
case 'o':
pargs.output_file = fopen(arg, "w");
if (pargs.output_file == NULL) {
@@ -677,6 +737,7 @@ cb(const char opt, const char * arg) {
pargs.heap > LONG_MAX
)
arg_error("the heap size must be a positive integer");
+ pargs.heap <<= 20;
break;
case '?':
diff --git a/src/argparse.h b/src/argparse.h
index aea98c8d..14b9cebe 100644
--- a/src/argparse.h
+++ b/src/argparse.h
@@ -34,9 +34,15 @@ typedef struct {
ctime_t t_sampling_interval;
ctime_t timeout;
pid_t attach_pid;
+ int where;
int exclude_empty;
int sleepless;
char * format;
+ #ifdef NATIVE
+ char * native_format;
+ char * kernel_format;
+ #endif
+ char * head_format;
int full;
int memory;
FILE * output_file;
diff --git a/src/austin.1 b/src/austin.1
index 8d9821e6..4e63ce3e 100644
--- a/src/austin.1
+++ b/src/austin.1
@@ -1,12 +1,14 @@
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.13.
-.TH AUSTIN "1" "December 2021" "austin 3.2.0" "User Commands"
+.TH AUSTIN "1" "January 2022" "austin 3.3.0" "User Commands"
.SH NAME
-austin \- manual page for austin 3.2.0
+austin \- Frame stack sampler for CPython
.SH SYNOPSIS
.B austin
[\fI\,OPTION\/\fR...] \fI\,command \/\fR[\fI\,ARG\/\fR...]
.SH DESCRIPTION
-Austin \fB\-\-\fR A frame stack sampler for Python.
+Austin is a frame stack sampler for CPython that is used to extract profiling
+data out of a running Python process (and all its children, if required) that
+requires no instrumentation and has practically no impact on the tracee.
.TP
\fB\-a\fR, \fB\-\-alt\-format\fR
Alternative collapsed stack sample format.
@@ -26,7 +28,7 @@ Sample the garbage collector state.
.TP
\fB\-h\fR, \fB\-\-heap\fR=\fI\,n_mb\/\fR
Maximum heap size to allocate to increase sampling
-accuracy, in MB (default is 256).
+accuracy, in MB (default is 0).
.TP
\fB\-i\fR, \fB\-\-interval\fR=\fI\,n_us\/\fR
Sampling interval in microseconds (default is
@@ -39,8 +41,7 @@ Profile memory usage.
Specify an output file for the collected samples.
.TP
\fB\-p\fR, \fB\-\-pid\fR=\fI\,PID\/\fR
-The the ID of the process to which Austin should
-attach.
+Attach to the process with the given PID.
.TP
\fB\-P\fR, \fB\-\-pipe\fR
Pipe mode. Use when piping Austin output.
@@ -52,6 +53,10 @@ Suppress idle samples to estimate CPU time.
Start up wait time in milliseconds (default is
100). Accepted units: s, ms.
.TP
+\fB\-w\fR, \fB\-\-where\fR=\fI\,PID\/\fR
+Dump the stacks of all the threads within the
+process with the given PID.
+.TP
\fB\-x\fR, \fB\-\-exposure\fR=\fI\,n_sec\/\fR
Sample for n_sec seconds only.
.TP
@@ -66,6 +71,67 @@ Print program version
.PP
Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.
+.SH EXAMPLES
+.PP
+Profile wall time of a Python script
+.PP
+.RS
+$ austin python3 \fI\,myscript.py\fR
+.RE
+.PP
+Profile CPU time of an executable Python script
+.PP
+.RS
+$ austin -s \fI\,./myscript.py\/\fR
+.RE
+.PP
+Profile a Python application
+.PP
+.RS
+$ austin \fI\,uwsgi\fR --http :9090 --wsgi-file foobar.py
+.RE
+.PP
+Profile child processes
+.PP
+.RS
+$ austin \fI\,-C\fR uwsgi --http :9090 --wsgi-file foobar.py
+.RE
+.PP
+Attach to a running Python process
+.PP
+.RS
+# austin -p \fI\,\fR
+.RE
+.PP
+Attach to a process and its children
+.PP
+.RS
+# austin -Cp \fI\,\fR
+.RE
+.PP
+Where is a Python process at?
+.PP
+.RS
+# austin -w \fI\,\fR
+.RE
+.PP
+Set the sampling interval
+.PP
+.RS
+# austin -i \fI\,10ms\fR -p
+.RE
+.PP
+Save collected on-CPU samples to file
+.PP
+.RS
+$ austin -so \fI/path/to/file.austin\fR ./myscript.py
+.RE
+.PP
+Sample for 5 seconds only
+.PP
+.RS
+# austin -x \fI5\fR -p
+.RE
.SH "REPORTING BUGS"
Report bugs to .
.SH "SEE ALSO"
diff --git a/src/austin.c b/src/austin.c
index ee431ac3..ae29cc60 100644
--- a/src/austin.c
+++ b/src/austin.c
@@ -38,7 +38,7 @@
#include "mem.h"
#include "msg.h"
#include "platform.h"
-#include "python.h"
+#include "python/abi.h"
#include "stats.h"
#include "timing.h"
#include "version.h"
@@ -65,9 +65,12 @@ signal_callback_handler(int signum)
// ----------------------------------------------------------------------------
void
do_single_process(py_proc_t * py_proc) {
- log_meta_header();
+ if (!pargs.where)
+ log_meta_header();
+
py_proc__log_version(py_proc, TRUE);
- NL;
+ if (!pargs.where)
+ NL;
if (pargs.exposure == 0) {
while(interrupt == FALSE) {
@@ -84,7 +87,8 @@ do_single_process(py_proc_t * py_proc) {
}
}
else {
- log_m("🕑 Sampling for %d second%s", pargs.exposure, pargs.exposure != 1 ? "s" : "");
+ if (!pargs.where && !pargs.pipe)
+ log_m("🕑 Sampling for %d second%s", pargs.exposure, pargs.exposure != 1 ? "s" : "");
ctime_t end_time = gettime() + pargs.exposure * 1000000;
while(interrupt == FALSE) {
stopwatch_start();
@@ -98,7 +102,7 @@ do_single_process(py_proc_t * py_proc) {
stopwatch_pause(stopwatch_duration());
#endif
- if (end_time < gettime())
+ if (end_time < gettime() || pargs.where)
interrupt++;
}
}
@@ -158,7 +162,10 @@ do_child_processes(py_proc_t * py_proc) {
}
}
- log_meta_header();NL;
+ if (!pargs.where) {
+ log_meta_header();
+ NL;
+ }
if (pargs.exposure == 0) {
while (!py_proc_list__is_empty(list) && interrupt == FALSE) {
@@ -175,7 +182,8 @@ do_child_processes(py_proc_t * py_proc) {
}
}
else {
- log_m("🕑 Sampling for %d second%s", pargs.exposure, pargs.exposure != 1 ? "s" : "");
+ if (!pargs.pipe && !pargs.where)
+ log_m("🕑 Sampling for %d second%s", pargs.exposure, pargs.exposure != 1 ? "s" : "");
ctime_t end_time = gettime() + pargs.exposure * 1000000;
while (!py_proc_list__is_empty(list) && interrupt == FALSE) {
#ifndef NATIVE
@@ -189,7 +197,8 @@ do_child_processes(py_proc_t * py_proc) {
stopwatch_pause(gettime() - start_time);
#endif
- if (end_time < gettime()) interrupt++;
+ if (end_time < gettime() || pargs.where)
+ interrupt++;
}
}
@@ -226,7 +235,7 @@ int main(int argc, char ** argv) {
goto finally;
}
- py_proc = py_proc_new();
+ py_proc = py_proc_new(FALSE);
if (!isvalid(py_proc)) {
log_ie("Cannot create process");
goto finally;
@@ -252,7 +261,7 @@ int main(int argc, char ** argv) {
}
} else {
if (
- fail(py_proc__attach(py_proc, pargs.attach_pid, FALSE))
+ fail(py_proc__attach(py_proc, pargs.attach_pid))
&& !pargs.children
) {
log_ie("Cannot attach the process");
@@ -267,13 +276,23 @@ int main(int argc, char ** argv) {
if (pargs.output_file != stdout)
log_i("Output file: %s", pargs.output_filename);
- log_i("Sampling interval: %lu μs", pargs.t_sampling_interval);
+ if (pargs.where) {
+ log_i("Where mode on process %d", pargs.attach_pid);
+ pargs.t_sampling_interval = 1;
+ // We use the exposure branch to emulate sampling once
+ pargs.exposure = 1;
+ }
+ else
+ log_i("Sampling interval: %lu μs", pargs.t_sampling_interval);
+
+ if (pargs.heap)
+ log_i("Maximum frame heap size: %d MB", pargs.heap >> 20);
if (pargs.full) {
if (pargs.memory)
log_w("The memory switch is redundant in full mode");
if (pargs.sleepless)
- log_w("The sleepless switch is reduntant in full mode");
+ log_w("The sleepless switch is redundant in full mode");
log_i("Producing full set of metrics (time +mem -mem)");
pargs.memory = TRUE;
}
@@ -306,6 +325,9 @@ int main(int argc, char ** argv) {
if (austin_errno == EPROCNPID)
austin_errno = EOK;
+ if (pargs.where)
+ goto finally;
+
// Log sampling metrics
NL;
meta("duration: %lu", stats_duration());
diff --git a/src/austin.h b/src/austin.h
index da5561d4..4350bb15 100644
--- a/src/austin.h
+++ b/src/austin.h
@@ -24,6 +24,6 @@
#define AUSTIN_H
#define PROGRAM_NAME "austin"
-#define VERSION "3.2.0"
+#define VERSION "3.3.0"
#endif
diff --git a/src/cache.c b/src/cache.c
new file mode 100644
index 00000000..292f67dc
--- /dev/null
+++ b/src/cache.c
@@ -0,0 +1,406 @@
+// 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) 2018-2021 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 .
+
+#ifdef DEBUG
+#include
+#endif
+#include
+#include
+
+#include "cache.h"
+#include "logging.h"
+
+// -- Queue -------------------------------------------------------------------
+
+// ----------------------------------------------------------------------------
+queue_item_t *
+queue_item_new(value_t value, key_dt key) {
+ queue_item_t *item = (queue_item_t *)calloc(1, sizeof(queue_item_t));
+
+ item->value = value;
+ item->key = key;
+
+ return item;
+}
+
+// ----------------------------------------------------------------------------
+void
+queue_item__destroy(queue_item_t * self, void (*deallocator)(value_t)) {
+ if (!isvalid(self))
+ return;
+
+ deallocator(self->value);
+
+ free(self);
+}
+
+// ----------------------------------------------------------------------------
+queue_t *
+queue_new(int capacity, void (*deallocator)(value_t)) {
+ queue_t *queue = (queue_t *)calloc(1, sizeof(queue_t));
+
+ queue->capacity = capacity;
+ queue->deallocator = deallocator;
+
+ return queue;
+}
+
+// ----------------------------------------------------------------------------
+int
+queue__is_full(queue_t *queue) {
+ return queue->count == queue->capacity;
+}
+
+// ----------------------------------------------------------------------------
+int
+queue__is_empty(queue_t *queue) {
+ return queue->rear == NULL;
+}
+
+// ----------------------------------------------------------------------------
+value_t
+queue__dequeue(queue_t *queue) {
+ if (queue__is_empty(queue))
+ return NULL;
+
+ if (queue->front == queue->rear)
+ queue->front = NULL;
+
+ queue_item_t *temp = queue->rear;
+ queue->rear = queue->rear->prev;
+
+ if (queue->rear)
+ queue->rear->next = NULL;
+
+ void *value = temp->value;
+ free(temp);
+
+ queue->count--;
+
+ return value;
+}
+
+// ----------------------------------------------------------------------------
+queue_item_t *
+queue__enqueue(queue_t *self, value_t value, key_dt key) {
+ if (queue__is_full(self))
+ return NULL;
+
+ queue_item_t *temp = queue_item_new(value, key);
+ temp->next = self->front;
+
+ if (queue__is_empty(self))
+ self->rear = self->front = temp;
+ else {
+ self->front->prev = temp;
+ self->front = temp;
+ }
+
+ self->count++;
+
+ return temp;
+}
+
+// ----------------------------------------------------------------------------
+void
+queue__destroy(queue_t *self) {
+ if (!isvalid(self))
+ return;
+
+ queue_item_t * next = NULL;
+ for (queue_item_t *item = self->front; isvalid(item); item = next) {
+ next = item->next;
+ queue_item__destroy(item, self->deallocator);
+ }
+
+ free(self);
+}
+
+
+// -- Hash Table --------------------------------------------------------------
+
+// ----------------------------------------------------------------------------
+chain_t *
+chain_new(key_dt key, value_t value) {
+ chain_t *chain = (chain_t *)calloc(1, sizeof(chain_t));
+
+ chain->key = key;
+ chain->value = value;
+
+ return chain;
+}
+
+// ----------------------------------------------------------------------------
+int
+chain__add(chain_t *self, key_dt key, value_t value) {
+ if (!isvalid(self))
+ return 0;
+
+ if (!isvalid(self->next)) {
+ self->next = chain_new(key, value);
+ return 1;
+ }
+
+ if (self->next->key == key) {
+ self->next->value = value;
+ } else
+ return chain__add(self->next, key, value);
+
+ return 0;
+}
+
+// ----------------------------------------------------------------------------
+int
+chain__remove(chain_t * self, key_dt key) {
+ if (!isvalid(self) || !isvalid(self->next))
+ return FALSE;
+
+ if (self->next->key == key) {
+ chain_t * next = self->next;
+ self->next = next->next;
+ next->next = NULL;
+
+ free(next);
+
+ return TRUE;
+ }
+
+ return chain__remove(self->next, key);
+}
+
+// ----------------------------------------------------------------------------
+value_t
+chain__find(chain_t * self, key_dt key) {
+ if (!isvalid(self))
+ return NULL;
+
+ if (self->key == key)
+ return self->value;
+
+ return chain__find(self->next, key);
+}
+
+// ----------------------------------------------------------------------------
+int
+chain__has(chain_t * self, key_dt key) {
+ if (!isvalid(self))
+ return FALSE;
+
+ if (self->key == key)
+ return TRUE;
+
+ return chain__has(self->next, key);
+}
+
+// ----------------------------------------------------------------------------
+void chain__destroy(chain_t * self) {
+ if (!isvalid(self))
+ return;
+
+ chain__destroy(self->next);
+
+ free(self);
+}
+
+// ----------------------------------------------------------------------------
+hash_table_t *
+hash_table_new(int capacity) {
+ hash_table_t *hash = (hash_table_t *) calloc(1, sizeof(hash_table_t));
+
+ hash->capacity = capacity;
+ hash->chains = (chain_t **) calloc(hash->capacity, sizeof(chain_t *));
+
+ return hash;
+}
+
+// ----------------------------------------------------------------------------
+#define MAGIC 2654435761
+
+static inline index_t
+_hash_table__index(hash_table_t *self, key_dt key) {
+ return (uintptr_t)((key * MAGIC) % self->capacity);
+}
+
+// ----------------------------------------------------------------------------
+value_t
+hash_table__get(hash_table_t *self, key_dt key) {
+ if (!isvalid(self))
+ return NULL;
+
+ chain_t * chain = self->chains[_hash_table__index(self, key)];
+ if (!isvalid(chain))
+ return NULL;
+
+ return chain__find(chain, key);
+}
+
+// ----------------------------------------------------------------------------
+#ifdef DEBUG
+static unsigned int _set_total = 0;
+static unsigned int _set_empty = 0;
+#endif
+
+void
+hash_table__set(hash_table_t *self, key_dt key, value_t value) {
+ if (!isvalid(self))
+ return;
+
+ index_t index = _hash_table__index(self, key);
+
+ #ifdef DEBUG
+ _set_total++;
+ #endif
+
+ chain_t * chain = self->chains[index];
+ if (!isvalid(chain)) {
+ if (self->size >= self->capacity)
+ return;
+ #ifdef DEBUG
+ _set_empty++;
+ #endif
+ self->chains[index] = chain_head();
+ self->size += chain__add(self->chains[index], key, value);
+ return;
+ }
+
+ if ((self->size >= self->capacity) && !chain__has(chain, key))
+ return;
+
+ self->size += chain__add(self->chains[index], key, value);
+}
+
+// ----------------------------------------------------------------------------
+void
+hash_table__del(hash_table_t * self, key_dt key) {
+ if (!isvalid(self) || self->size == 0)
+ return;
+
+ index_t index = _hash_table__index(self, key);
+ chain_t * chain = self->chains[index];
+
+ if (!isvalid(chain))
+ return;
+
+ self->size -= chain__remove(chain, key);
+
+ if (chain__is_empty(chain)) {
+ chain__destroy(chain);
+ self->chains[index] = NULL;
+ }
+}
+
+// ----------------------------------------------------------------------------
+void
+hash_table__destroy(hash_table_t *self) {
+ if (!isvalid(self))
+ return;
+
+ if (isvalid(self->chains)) {
+ for (int i = 0; i < self->capacity; chain__destroy(self->chains[i++]));
+ sfree(self->chains);
+ }
+
+ free(self);
+}
+
+
+// -- LRU Cache ---------------------------------------------------------------
+
+// ----------------------------------------------------------------------------
+lru_cache_t *
+lru_cache_new(int capacity, void (*deallocator)(value_t)) {
+ lru_cache_t *cache = (lru_cache_t *)calloc(1, sizeof(lru_cache_t));
+
+ cache->capacity = capacity;
+ cache->queue = queue_new(capacity, deallocator);
+ cache->hash = hash_table_new((capacity * 4 / 3) | 1);
+
+ return cache;
+}
+
+// ----------------------------------------------------------------------------
+value_t
+lru_cache__maybe_hit(lru_cache_t *self, key_dt key) {
+ queue_item_t *item = (queue_item_t *)hash_table__get(self->hash, key);
+
+ if (!isvalid(item))
+ return NULL;
+
+ // Bring hit element to the front of the queue
+ if (item != self->queue->front)
+ {
+ item->prev->next = item->next;
+ if (item->next)
+ item->next->prev = item->prev;
+
+ if (item == self->queue->rear)
+ {
+ self->queue->rear = item->prev;
+ self->queue->rear->next = NULL;
+ }
+
+ item->next = self->queue->front;
+ item->prev = NULL;
+
+ item->next->prev = item;
+
+ self->queue->front = item;
+ }
+
+ return item->value;
+}
+
+// ----------------------------------------------------------------------------
+void
+lru_cache__store(lru_cache_t *self, key_dt key, value_t value) {
+ queue_t * queue = self->queue;
+
+ if (queue__is_full(queue)) {
+ hash_table__del(self->hash, queue->rear->key);
+
+ value_t value = queue__dequeue(queue);
+ if (isvalid(value))
+ queue->deallocator(value);
+ }
+
+ hash_table__set(self->hash, key, queue__enqueue(self->queue, value, key));
+}
+
+// ----------------------------------------------------------------------------
+void
+lru_cache__destroy(lru_cache_t *self) {
+ if (!isvalid(self))
+ return;
+
+ log_d(
+ "LRU cache collisions: %d/%d (%0.2f%%, prob: %0.2f%%)\n",
+ _set_total - _set_empty,
+ _set_total,
+ (_set_total - _set_empty) * 100.0 / _set_total,
+ 100.0 * (1 - exp(-((double) self->queue->count) * (self->queue->count - 1.0) / 2.0 / self->hash->capacity))
+ );
+
+ queue__destroy(self->queue);
+ hash_table__destroy(self->hash);
+
+ free(self);
+}
diff --git a/src/cache.h b/src/cache.h
new file mode 100644
index 00000000..08490b5b
--- /dev/null
+++ b/src/cache.h
@@ -0,0 +1,400 @@
+// 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) 2018-2021 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 .
+
+#ifndef CACHE_H
+#define CACHE_H
+
+#include
+
+#include "hints.h"
+
+typedef uintptr_t key_dt;
+typedef void * value_t;
+
+
+// -- Queue -------------------------------------------------------------------
+
+typedef struct queue_item_t {
+ struct queue_item_t *prev, *next;
+ key_dt key;
+ value_t value; // Takes ownership of a free-able object
+} queue_item_t;
+
+typedef struct queue_t {
+ unsigned count;
+ unsigned capacity;
+ queue_item_t *front, *rear;
+ void (*deallocator)(value_t);
+} queue_t;
+
+
+/**
+ * Create a new queue item.
+ *
+ * Note that the newly created item is meant to take ownership of the value.
+ *
+ * @param value_t The value to add.
+ * @param key_dt An optional integer key that identifies the value.
+ *
+ * @return a reference to a valid queue item, NULL otherwise.
+ */
+queue_item_t *
+queue_item_new(value_t, key_dt);
+
+
+/**
+ * Destroy a queue item.
+ *
+ * Since the queue item has ownership of the value, a destructor needs to be
+ * passed in order to deallocate the owned value.
+ *
+ * @param self the queue item
+ * @param deallocator the deallocator
+ */
+void
+queue_item__destroy(queue_item_t *, void (*)(value_t));
+
+
+/**
+ * Create a new queue object.
+ *
+ * Any element added to the queue will be owned by the queue. This means that,
+ * when the queue is destroyed, all the elements in it are also destroyed,
+ * according to the given deallocator.
+ *
+ * @param capacity the queue maximum capacity
+ * @param deallocator the queue item value deallocator
+ *
+ * @return a valid reference to a queue object, NULL otherwise.
+ */
+queue_t *
+queue_new(int, void (*)(value_t));
+
+
+/**
+ * Check if the queue is full.
+ *
+ * @param self the queue
+ *
+ * @return TRUE if the queue is full, else FALSE.
+ */
+int
+queue__is_full(queue_t *);
+
+
+/**
+ * Check if the queue is empty.
+ *
+ * @param self the queue
+ *
+ * @return TRUE if the queue is empty, else FALSE.
+ */
+int
+queue__is_empty(queue_t *);
+
+
+/**
+ * Remove the first element in the queue.
+ *
+ * @param self the queue
+ *
+ * @return the value stored in the queue item (if any), else NULL.
+ */
+value_t
+queue__dequeue(queue_t *);
+
+
+/**
+ * Add an element to the queue.
+ *
+ * @param self the queue
+ * @param value the value to add
+ * @param key optional key to associate to the element
+ *
+ * @return a reference to the queue item, if the queue was not full, else NULL.
+ */
+queue_item_t *
+queue__enqueue(queue_t *, value_t, key_dt);
+
+
+/**
+ * Destroy the queue.
+ *
+ * @param self the queue
+ */
+void
+queue__destroy(queue_t *);
+
+
+// -- Hash Table --------------------------------------------------------------
+
+typedef unsigned int index_t;
+
+typedef struct _chain_t {
+ struct _chain_t *next;
+ key_dt key;
+ value_t value;
+} chain_t;
+
+typedef struct hash_table_t {
+ size_t capacity;
+ size_t size;
+ chain_t **chains;
+} hash_table_t;
+
+
+/**
+ * Create a new chain.
+ *
+ * This is an implementation of a linked list that used to implement chaining
+ * for resolving collisions in a hash table and therefore should be regarded
+ * as an internal detail implementation of the hash_table_t structure.
+ *
+ * A chain is an element in the list *and* the list itself. This is why this
+ * constructor takes a key and a value.
+ *
+ * NOTE: Ideally, a chain should be started with a sentinel element. The
+ * chain_head is a convenience macro to create chain heads.
+ *
+ * @param key the item key
+ * @param value the item value
+ *
+ * @return a valid reference to a chain, NULL otherwise.
+ */
+chain_t *
+chain_new(key_dt, value_t);
+
+
+/**
+ * Create a chain head item.
+ *
+ * @return a valid reference to a chain head, NULL otherwise.
+ */
+#define chain_head() (chain_new(0, NULL))
+
+
+/**
+ * Check if a chain is empty.
+ *
+ * Under the assumption that each chain starts with a chain head, the check
+ * involves the next element in the chain.
+ *
+ * @param self the chain.
+ *
+ * @return TRUE if the chain is empty (i.e. there is no next element), FALSE
+ * otherwise.
+ */
+#define chain__is_empty(chain) (!isvalid(chain->next))
+
+
+/**
+ * Add a new item to the chain.
+ *
+ * @param self the chain to add to
+ * @param key the key for the new item
+ * @param value the value for the new item
+ */
+int
+chain__add(chain_t *, key_dt, value_t);
+
+
+/**
+ * Remove an element from a chain given its key.
+ *
+ * @param self the chain to remove from
+ * @param key the key to match
+ *
+ * @return 1 if a chain item was removed, 0 otherwise.
+ */
+int
+chain__remove(chain_t *, key_dt);
+
+
+/**
+ * Find a value in the chain given its key.
+ *
+ * NOTE: If you want to check whether a chain has an element or not, the right
+ * method to use is chain__has since NULL is a valid chain item value.
+ *
+ * @param self the chain to search
+ * @param key the key to matck
+ *
+ * @return the value for the key if found, NULL otherwise.
+ */
+value_t
+chain__find(chain_t *, key_dt);
+
+
+/**
+ * Check whether a chain has an item with the given key.
+ *
+ * @param self the chain to check
+ * @param key the key to match.
+ *
+ * @return TRUE if the chain has an item with the given key, FALSE otherwise.
+ */
+int
+chain__has(chain_t *, key_dt);
+
+
+/**
+ * Deallocate a chain.
+ *
+ * @param self the chain to deallocate.
+ */
+void
+chain__destroy(chain_t *);
+
+
+/**
+ * Create a new hash table.
+ *
+ * @param capacity the hash table maximum capacity
+ *
+ * @return a valid reference to a new hash table, NULL otherwise.
+ */
+hash_table_t *
+hash_table_new(int);
+
+
+/**
+ * Get from the hash table.
+ *
+ * NOTE: This method cannot be used to determine whether a hash table has a
+ * given key, unless all the items have a non-NULL value.
+ *
+ * @param self the hash table
+ * @param key the key to match
+ *
+ * @return the value stored with the given key, NULL otherwise.
+ */
+value_t
+hash_table__get(hash_table_t *, key_dt);
+
+
+/**
+ * Set a value into the table.
+ *
+ * If the key is not already present and the table is full, this method does
+ * nothing.
+ *
+ * @param self the hash table to set into
+ * @param key the key to set at
+ * @param value the value to set
+ */
+void
+hash_table__set(hash_table_t *, key_dt, value_t);
+
+
+#define hash_table__iter_start(table) \
+ for (int i = 0; i < table->capacity; i++) { \
+ chain_t * chain = table->chains[i]; \
+ if (!isvalid(chain)) \
+ continue; \
+ while (isvalid(chain->next)) { \
+ chain = chain->next; \
+ value_t value = chain->value; \
+
+
+#define hash_table__iter_stop(table) }}
+
+
+/**
+ * Remove a value from the hash table.
+ *
+ * @param self the hash table to remove from
+ * @param key the key to remove
+ */
+void
+hash_table__del(hash_table_t *, key_dt);
+
+
+/**
+ * Deallocate a hash table.
+ *
+ * @param self the hash table to deallocate
+ */
+void
+hash_table__destroy(hash_table_t *);
+
+
+// -- LRU Cache ---------------------------------------------------------------
+
+typedef struct {
+ int capacity;
+ queue_t *queue;
+ hash_table_t *hash;
+} lru_cache_t;
+
+
+/**
+ * Create an LRU cache.
+ *
+ * Internally, this makes use of a queue which takes ownership of the values
+ * added to it. Therefore, the cache itself takes ownership of every value that
+ * is stored within it.
+ *
+ * @param capacity the cache capacity
+ * @param deallocator the value deallocator
+ *
+ * @return a valid reference to a cache, NULL otherwise.
+ */
+lru_cache_t *
+lru_cache_new(int, void (*)(value_t));
+
+
+/**
+ * Try to hit the cache.
+ *
+ * Since this method returns NULL on a cache miss, this only makes sense if all
+ * the objects stored within the cache are non-NULL.
+ *
+ * @param self the cache to hit
+ * @param key the key to search
+ *
+ * @return the value associated to the key if a hit occurred, NULL otherwise.
+ */
+value_t
+lru_cache__maybe_hit(lru_cache_t *, key_dt);
+
+
+/**
+ * Store a value within the cache at the given key. If the cache is full, the
+ * least recently used key/value pair is evicted.
+ *
+ * @param self the cache to store into
+ * @param key the key at which to store the value
+ * @param value the value to store
+ */
+void
+lru_cache__store(lru_cache_t *, key_dt, value_t);
+
+
+/**
+ * Deallocate a cache.
+ *
+ * @param self the cache to deallocate
+ */
+void
+lru_cache__destroy(lru_cache_t *);
+
+#endif
\ No newline at end of file
diff --git a/src/error.c b/src/error.c
index 6f1cbbfb..115d338e 100644
--- a/src/error.c
+++ b/src/error.c
@@ -42,7 +42,7 @@ const char * _error_msg_tab[MAXERROR] = {
NULL,
NULL,
- // py_code_t
+ // PyCodeObject
"Failed to retrieve PyCodeObject",
"Encountered unsupported string format",
"Not a compact unicode object",
@@ -52,7 +52,7 @@ const char * _error_msg_tab[MAXERROR] = {
"Unable to get line number from code object",
"Failed to retrieve PyUnicodeObject",
- // py_frame_t
+ // PyFrameObject
"Failed to create frame object",
"Failed to get code object for frame",
"Invalid frame",
@@ -95,7 +95,7 @@ const int _fatal_error_tab[MAXERROR] = {
0,
0,
- // py_code_t
+ // PyCodeObject
0,
0,
0,
@@ -105,7 +105,7 @@ const int _fatal_error_tab[MAXERROR] = {
0,
0,
- // py_frame_t
+ // PyFrameObject
0,
0,
0,
diff --git a/src/error.h b/src/error.h
index b0438cc9..7fd69e92 100644
--- a/src/error.h
+++ b/src/error.h
@@ -36,7 +36,7 @@
#define ENULLDEV 4
#define ECMDLINE 5
-// py_code_t
+// PyCodeObject
#define ECODE ((1 << 3) + 0)
#define ECODEFMT ((1 << 3) + 1)
#define ECODECMPT ((1 << 3) + 2)
@@ -46,7 +46,7 @@
#define ECODENOLINENO ((1 << 3) + 6)
#define ECODEUNICODE ((1 << 3) + 7)
-// py_frame_t
+// PyFrameObject
#define EFRAME ((2 << 3) + 0)
#define EFRAMENOCODE ((2 << 3) + 1)
#define EFRAMEINV ((2 << 3) + 2)
diff --git a/src/heap.h b/src/heap.h
index 7a7162be..79865072 100644
--- a/src/heap.h
+++ b/src/heap.h
@@ -38,4 +38,6 @@ typedef struct {
size_t size;
} _heap_t;
+#define NULL_MEM_BLOCK ((_mem_block_t) {(void *) -1, NULL, (void *) -1, NULL})
+
#endif
diff --git a/src/hints.h b/src/hints.h
index 84cf64bd..9e4257ac 100644
--- a/src/hints.h
+++ b/src/hints.h
@@ -47,7 +47,7 @@
#define with_resources int retval = 0;
#define OK goto release;
-#define NOK retval = 1; goto release;
+#define NOK {retval = 1; goto release;}
#define released return retval;
#endif
diff --git a/src/linux/addr2line.h b/src/linux/addr2line.h
new file mode 100644
index 00000000..36072941
--- /dev/null
+++ b/src/linux/addr2line.h
@@ -0,0 +1,230 @@
+// 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) 2018-2022 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 .
+
+// This source has been adapted from
+// https://github.com/bminor/binutils-gdb/blob/ce230579c65b9e04c830f35cb78ff33206e65db1/binutils/addr2line.c
+
+#include
+#include
+#include
+#include
+#include
+#ifdef HAVE_LIBERTY
+#include
+#endif
+
+#include "../logging.h"
+#include "../stack.h"
+
+static asymbol **syms; /* Symbol table. */
+
+static void slurp_symtab(bfd *);
+static void find_address_in_section(bfd *, asection *, void *);
+
+#define string__startswith(str, head) (strncmp(head, str, strlen(head)) == 0)
+
+// TODO: This is incomplete or plain incorrect
+static inline char *
+demangle_cython(char *function)
+{
+ if (!isvalid(function))
+ return NULL;
+
+ char *f = function;
+
+ if (string__startswith(f, "__pyx_pw_") || string__startswith(f, "__pyx_pf_"))
+ return function;
+
+ if (string__startswith(function, "__pyx_pymod_"))
+ return strchr(f + 12, '_') + 1;
+
+ if (string__startswith(f, "__pyx_fuse_"))
+ function = strstr(f + 12, "__pyx_") + 12;
+
+ while (!isdigit(*f))
+ {
+ if (*(f++) == '\0')
+ return function;
+ }
+
+ int n = 0;
+ while (*f != '\0')
+ {
+ puts(f);
+ char c = *(f++);
+ if (isdigit(c))
+ n = n * 10 + (c - '0');
+ else
+ {
+ f += n;
+ n = 0;
+ if (!isdigit(*f))
+ return f;
+ }
+ }
+
+ return function;
+}
+
+/* Read in the symbol table. */
+
+static void
+slurp_symtab(bfd *abfd)
+{
+ long storage;
+ long symcount;
+ bool dynamic = false;
+
+ if ((bfd_get_file_flags(abfd) & HAS_SYMS) == 0)
+ return;
+
+ storage = bfd_get_symtab_upper_bound(abfd);
+ if (storage == 0)
+ {
+ storage = bfd_get_dynamic_symtab_upper_bound(abfd);
+ dynamic = true;
+ }
+ if (storage < 0)
+ return;
+
+ syms = (asymbol **)malloc(storage);
+ if (dynamic)
+ symcount = bfd_canonicalize_dynamic_symtab(abfd, syms);
+ else
+ symcount = bfd_canonicalize_symtab(abfd, syms);
+ if (symcount < 0)
+ return;
+
+ /* If there are no symbols left after canonicalization and
+ we have not tried the dynamic symbols then give them a go. */
+ if (symcount == 0 && !dynamic && (storage = bfd_get_dynamic_symtab_upper_bound(abfd)) > 0)
+ {
+ free(syms);
+ syms = (asymbol **)malloc(storage);
+ symcount = bfd_canonicalize_dynamic_symtab(abfd, syms);
+ }
+
+ /* PR 17512: file: 2a1d3b5b.
+ Do not pretend that we have some symbols when we don't. */
+ if (symcount <= 0)
+ {
+ free(syms);
+ syms = NULL;
+ }
+}
+
+static bfd_vma pc;
+static const char *filename;
+static const char *functionname;
+static unsigned int line;
+static unsigned int discriminator;
+
+/* Look for an address in a section. This is called via
+ bfd_map_over_sections. */
+
+static void
+find_address_in_section(bfd *abfd, asection *section, void *data ATTRIBUTE_UNUSED)
+{
+ bfd_vma vma;
+ bfd_size_type size;
+
+ if ((bfd_section_flags(section) & SEC_ALLOC) == 0)
+ return;
+
+ vma = bfd_section_vma(section);
+ if (pc < vma)
+ return;
+
+ size = bfd_section_size(section);
+ if (pc >= vma + size)
+ return;
+
+ bfd_find_nearest_line_discriminator(abfd, section, syms, pc - vma,
+ &filename, &functionname,
+ &line, &discriminator);
+}
+
+static inline frame_t *
+get_native_frame(const char *file_name, bfd_vma addr)
+{
+ bfd *abfd;
+ char **matching;
+
+ // TODO: This would be much cheaper if we could read directly from memory.
+ abfd = bfd_openr(file_name, NULL);
+ if (abfd == NULL)
+ {
+ log_e("Failed to open %s", file_name);
+ return NULL;
+ }
+
+ /* Decompress sections. */
+ abfd->flags |= BFD_DECOMPRESS;
+
+ if (bfd_check_format(abfd, bfd_archive))
+ {
+ log_e("BFD format check failed");
+ return NULL;
+ }
+
+ if (!bfd_check_format_matches(abfd, bfd_object, &matching))
+ {
+ free(matching);
+ log_d("BFC format matches check failed.");
+ return NULL;
+ }
+
+ slurp_symtab(abfd);
+
+ // Reset global state for a new lookup
+ filename = functionname = NULL;
+ line = discriminator = 0;
+ pc = addr;
+
+ bfd_map_over_sections(abfd, find_address_in_section, NULL);
+
+ const char *name;
+ char *alloc = NULL;
+
+ name = functionname;
+ if (name == NULL || *name == '\0')
+ name = "";
+#ifdef HAVE_LIBERTY
+ else
+ {
+ alloc = bfd_demangle(abfd, name, DMGL_PARAMS | DMGL_ANSI);
+ if (alloc != NULL)
+ name = alloc;
+ }
+#endif
+
+ free(syms);
+ syms = NULL;
+
+ frame_t *frame = isvalid(filename) && isvalid(name)
+ ? frame_new(strdup(filename), strdup(name), line)
+ : NULL;
+
+ bfd_close(abfd);
+
+ return frame;
+}
diff --git a/src/dict.c b/src/linux/common.h
similarity index 50%
rename from src/dict.c
rename to src/linux/common.h
index 4d6a6dea..0619bcc0 100644
--- a/src/dict.c
+++ b/src/linux/common.h
@@ -5,7 +5,7 @@
//
// Austin is a Python frame stack sampler for CPython.
//
-// Copyright (c) 2018 Gabriele N. Tornetta .
+// Copyright (c) 2018-2021 Gabriele N. Tornetta .
// All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
@@ -20,22 +20,44 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-#include
-#include
+#ifndef COMMON_H
+#define COMMON_H
-#define MAGIC_TINY 7
-#define MAGIC_BIG 1000003
+#include
+#include
-// Stolen from stringobject.c
-long
-string_hash(char * string) {
- register unsigned char *p;
- register long x;
+#include "../stats.h"
- p = (unsigned char *) string;
- x = *p << MAGIC_TINY;
- while (*p != 0)
- x = (MAGIC_BIG * x) ^ *(p++);
- x ^= strlen(string);
- return x == 0 ? 1 : x;
+
+#define PTHREAD_BUFFER_ITEMS 200
+
+static uintptr_t _pthread_buffer[PTHREAD_BUFFER_ITEMS];
+
+#define read_pthread_t(pid, addr) \
+ (copy_memory(pid, addr, sizeof(_pthread_buffer), _pthread_buffer))
+
+
+struct _proc_extra_info {
+ unsigned int page_size;
+ char statm_file[24];
+ pthread_t wait_thread_id;
+ unsigned int pthread_tid_offset;
+};
+
+
+#ifdef NATIVE
+#include
+
+static inline int
+wait_ptrace(enum __ptrace_request request, pid_t pid, void * addr, void * data) {
+ int outcome = 0;
+ ctime_t end = gettime() + 1000;
+ while (gettime() < end && (outcome = ptrace(request, pid, addr, data)) && errno == 3)
+ sched_yield();
+ return outcome;
}
+
+#endif
+
+
+#endif
diff --git a/src/linux/py_proc.h b/src/linux/py_proc.h
index a98a5db4..2c04d127 100644
--- a/src/linux/py_proc.h
+++ b/src/linux/py_proc.h
@@ -35,12 +35,16 @@
#include
#include
+#include "common.h"
+
#ifdef NATIVE
#include "../argparse.h"
+#include "../cache.h"
#endif
-#include "../dict.h"
+#include "../py_string.h"
#include "../hints.h"
#include "../py_proc.h"
+#include "../version.h"
#define CHECK_HEAP
@@ -62,11 +66,6 @@
#define ELF_SH_OFF(ehdr, i) /* as */ (ehdr->e_shoff + i * ehdr->e_shentsize)
-struct _proc_extra_info {
- unsigned int page_size;
- char statm_file[24];
- pthread_t wait_thread_id;
-};
union {
@@ -479,24 +478,43 @@ _py_proc__get_resident_memory(py_proc_t * self) {
return -1;
}
+ int ret = 0;
+
ssize_t size, resident;
if (fscanf(statm, "%ld %ld", &size, &resident) != 2)
- return -1;
+ ret = -1;
fclose(statm);
- return resident * self->extra->page_size;
+ return ret ? ret : resident * self->extra->page_size;
} /* _py_proc__get_resident_memory */
#ifdef NATIVE
// ----------------------------------------------------------------------------
+char pathname[1024];
+char prevpathname[1024];
+vm_range_t * ranges[256];
+
static int
-_py_proc__dump_maps(py_proc_t * self) {
- char file_name[32];
- FILE * fp = NULL;
- char * line = NULL;
- size_t len = 0;
+_py_proc__get_vm_maps(py_proc_t * self) {
+ FILE * fp = NULL;
+ char * line = NULL;
+ size_t len = 0;
+ vm_range_tree_t * tree = NULL;
+ hash_table_t * table = NULL;
+ char file_name[32];
+
+ if (pargs.where) {
+ tree = vm_range_tree_new();
+ table = hash_table_new(256);
+
+ vm_range_tree__destroy(self->maps_tree);
+ hash_table__destroy(self->base_table);
+
+ self->maps_tree = tree;
+ self->base_table = table;
+ }
sprintf(file_name, "/proc/%d/maps", self->pid);
fp = fopen(file_name, "r");
@@ -514,23 +532,41 @@ _py_proc__dump_maps(py_proc_t * self) {
FAIL;
}
- while (getline(&line, &len, fp) != -1) {
+ log_d("Rebuilding vm ranges tree");
+
+ int nrange = 0;
+ while (getline(&line, &len, fp) != -1 && nrange < 256) {
ssize_t lower, upper;
- char pathname[1024];
if (sscanf(line, "%lx-%lx %*s %*x %*x:%*x %*x %s\n",
&lower, &upper, // Map bounds
pathname // Binary path
) == 3 && pathname[0] != '[') {
- fprintf(pargs.output_file, "# map: %lx-%lx %s\n", lower, upper, pathname);
+ if (pargs.where) {
+ if (strcmp(pathname, prevpathname)) {
+ ranges[nrange++] = vm_range_new(lower, upper, strdup(pathname));
+ key_dt key = string__hash(pathname);
+ if (!isvalid(hash_table__get(table, key)))
+ hash_table__set(table, key, (value_t) lower);
+ strcpy(prevpathname, pathname);
+ } else
+ ranges[nrange-1]->hi = upper;
+ }
+ else
+ // We print the maps instead so that we can resolve them later and use
+ // the CPU more efficiently to collect samples.
+ fprintf(pargs.output_file, "# map: %lx-%lx %s\n", lower, upper, pathname);
}
}
+ for (int i = 0; i < nrange; i++)
+ vm_range_tree__add(tree, (vm_range_t *) ranges[i]);
+
sfree(line);
fclose(fp);
SUCCESS;
-} /* _py_proc__dump_maps */
+} /* _py_proc__get_vm_maps */
#endif
@@ -550,11 +586,46 @@ _py_proc__init(py_proc_t * self) {
self->last_resident_memory = _py_proc__get_resident_memory(self);
#ifdef NATIVE
- _py_proc__dump_maps(self);
+ _py_proc__get_vm_maps(self);
#endif
SUCCESS;
} /* _py_proc__init */
+// Support for CPU time on Linux. We need to retrieve the TID from the struct
+// pthread pointed to by the native thread ID stored by Python. We do not have
+// the definition of the structure, so we need to "guess" the offset of the tid
+// field within struct pthread.
+
+// ----------------------------------------------------------------------------
+static int
+_infer_tid_field_offset(py_thread_t * py_thread) {
+ if (fail(read_pthread_t(py_thread->raddr.pid, (void *) py_thread->tid))) {
+ log_d("Cannot copy pthread_t structure");
+ FAIL;
+ }
+
+ log_d("pthread_t at %p", py_thread->tid);
+
+ for (register int i = 0; i < PTHREAD_BUFFER_ITEMS; i++) {
+ if (py_thread->raddr.pid == _pthread_buffer[i]) {
+ log_d("TID field offset: %d", i);
+ py_thread->proc->extra->pthread_tid_offset = i;
+ SUCCESS;
+ }
+ }
+
+ // Fall-back to smaller steps if we failed
+ for (register int i = 0; i < PTHREAD_BUFFER_ITEMS * (sizeof(uintptr_t) / sizeof(pid_t)); i++) {
+ if (py_thread->raddr.pid == (pid_t) ((pid_t *) _pthread_buffer)[i]) {
+ log_d("TID field offset (from fall-back): %d", i);
+ py_thread->proc->extra->pthread_tid_offset = -i;
+ SUCCESS;
+ }
+ }
+
+ FAIL;
+}
+
#endif
diff --git a/src/linux/py_thread.h b/src/linux/py_thread.h
index 0d673028..6c22b958 100644
--- a/src/linux/py_thread.h
+++ b/src/linux/py_thread.h
@@ -26,62 +26,19 @@
#include
#include
+#include "common.h"
+
#include "../hints.h"
#include "../py_thread.h"
-// Support for CPU time on Linux. We need to retrieve the TID from the the
-// struct pthread pointed to by the native thread ID stored by Python. We do not
-// have the definition of the structure, so we need to "guess" the offset of the
-// tid field within struct pthread.
-#define PTHREAD_BUFFER_SIZE 200
-
-
-static int _pthread_tid_offset = 0;
-void * _pthread_buffer[PTHREAD_BUFFER_SIZE];
-
-
-// ----------------------------------------------------------------------------
-static void
-_infer_tid_field_offset(py_thread_t * py_thread) {
- if (fail(copy_memory(
- py_thread->raddr.pid,
- (void *) py_thread->tid, // At this point this is still the pthread_t *
- PTHREAD_BUFFER_SIZE * sizeof(void *),
- _pthread_buffer
- ))) {
- log_d("Cannot copy pthread_t structure");
- return;
- }
-
- log_d("pthread_t at %p", py_thread->tid);
-
- for (register int i = 0; i < PTHREAD_BUFFER_SIZE; i++) {
- if (py_thread->raddr.pid == (uintptr_t) _pthread_buffer[i]) {
- log_d("TID field offset: %d", i);
- _pthread_tid_offset = i;
- return;
- }
- }
-
- // Fall-back to smaller steps if we failed
- for (register int i = 0; i < PTHREAD_BUFFER_SIZE * sizeof(uintptr_t) / sizeof(pid_t); i++) {
- if (py_thread->raddr.pid == (pid_t) ((pid_t*) _pthread_buffer)[i]) {
- log_d("TID field offset (from fall-back): %d", i);
- _pthread_tid_offset = i;
- return;
- }
- }
-}
-
-
// ----------------------------------------------------------------------------
static int
_py_thread__is_idle(py_thread_t * self) {
with_resources;
- char file_name[64];
- char buffer[2048];
+ char file_name[64];
+ char buffer[2048] = "";
retval = -1;
@@ -97,13 +54,17 @@ _py_thread__is_idle(py_thread_t * self) {
goto release;
}
- char * p = strchr(buffer, ')') + 2;
- if (p == NULL) {
+ char * p = strchr(buffer, ')');
+ if (!isvalid(p)) {
log_d("Invalid format for procfs file %s", file_name);
goto release;
}
- if (p[0] == ' ') ++p;
- retval = p[0] != 'R';
+
+ p+=2;
+ if (*p == ' ')
+ p++;
+
+ retval = (*p != 'R');
release:
close(fd);
diff --git a/src/linux/vm-range-tree.h b/src/linux/vm-range-tree.h
new file mode 100644
index 00000000..dd2a72cd
--- /dev/null
+++ b/src/linux/vm-range-tree.h
@@ -0,0 +1,237 @@
+// 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) 2018-2022 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 .
+
+#ifndef VM_RANGE_TREE_H
+#define VM_RANGE_TREE_H
+
+#include
+#include
+
+#include "../hints.h"
+
+typedef uintptr_t addr_t;
+
+typedef struct _vmrange {
+ addr_t lo, hi;
+ char * name;
+ struct _vmrange * left;
+ struct _vmrange * right;
+ int height;
+} vm_range_t;
+
+typedef struct{
+ vm_range_t *root;
+} vm_range_tree_t;
+
+#define max(a, b) ((a > b) ? a : b)
+#define vm_range__height(r) (isvalid(r) ? r->height : 0)
+
+#ifdef PY_PROC_C
+
+/**
+ * Create a new VM range.
+ *
+ * @param lo the range lower bound
+ * @param hi the range upper bound
+ * @param name the name of the VM map (takes ownership)
+ *
+ * @return a valid reference to a VM range object, NULL otherwise.
+ */
+vm_range_t *
+vm_range_new(addr_t lo, addr_t hi, char *name) {
+ vm_range_t *range = (vm_range_t *)malloc(sizeof(vm_range_t));
+
+ range->lo = lo;
+ range->hi = hi;
+ range->name = name;
+ range->left = NULL;
+ range->right = NULL;
+ range->height = 1;
+
+ return range;
+}
+
+/**
+ * Deallocate a VM range.
+ *
+ * @param self the VM range to deallocate.
+ */
+void
+vm_range__destroy(vm_range_t *self) {
+ if (!isvalid(self))
+ return;
+
+ sfree(self->name);
+
+ vm_range__destroy(self->left);
+ vm_range__destroy(self->right);
+
+ free(self);
+}
+
+
+static inline vm_range_t *
+_vm_range__rrot(vm_range_t *self) {
+ vm_range_t *x = self->left;
+ vm_range_t *T2 = x->right;
+
+ x->right = self;
+ self->left = T2;
+
+ self->height = max(vm_range__height(self->left), vm_range__height(self->right)) + 1;
+ x->height = max(vm_range__height(x->left), vm_range__height(x->right)) + 1;
+
+ return x;
+}
+
+
+static inline vm_range_t *
+_vm_range__lrot(vm_range_t *self) {
+ vm_range_t *y = self->right;
+ vm_range_t *T2 = y->left;
+
+ y->left = self;
+ self->right = T2;
+
+ self->height = max(vm_range__height(self->left), vm_range__height(self->right)) + 1;
+ y->height = max(vm_range__height(y->left), vm_range__height(y->right)) + 1;
+
+ return y;
+}
+
+
+static inline int
+_vm_range__bf(vm_range_t *self) {
+ return isvalid(self)
+ ? vm_range__height(self->left) - vm_range__height(self->right)
+ : 0;
+}
+
+static inline vm_range_t *
+_vm_range__add(vm_range_t *self, vm_range_t *range) {
+ if (!isvalid(self))
+ return range;
+
+ if (range->lo < self->lo)
+ self->left = _vm_range__add(self->left, range);
+ else
+ self->right = _vm_range__add(self->right, range);
+
+ self->height = 1 + max(vm_range__height(self->left), vm_range__height(self->right));
+
+ // Balance the tree
+ int balance = _vm_range__bf(self);
+ if (balance > 1)
+ {
+ if (range->lo < self->left->lo)
+ return _vm_range__rrot(self);
+ else
+ {
+ self->left = _vm_range__lrot(self->left);
+ return _vm_range__rrot(self);
+ }
+ }
+ else if (balance < -1)
+ {
+ if (range->lo > self->right->lo)
+ return _vm_range__lrot(self);
+ else
+ {
+ self->right = _vm_range__rrot(self->right);
+ return _vm_range__lrot(self);
+ }
+ }
+
+ return self;
+}
+
+
+/**
+ * Create a new VM range tree. This is an implementation of an AVL tree that
+ * is meant to store *non-overlapping* VM ranges for a fast look-up.
+ *
+ * @return a valid reference to a new VM range tree, NULL otherwise.
+ */
+vm_range_tree_t *
+vm_range_tree_new() {
+ return (vm_range_tree_t *)calloc(1, sizeof(vm_range_tree_t));
+}
+
+
+/**
+ * Add a new range to the VM range tree.
+ *
+ * The callee has the responsibility of ensuring that all the VM ranges that
+ * are added to this data structure are *non-overlapping*. Failure to comply to
+ * this constraint will make lookups fairly pointless.
+ */
+void
+vm_range_tree__add(vm_range_tree_t *self, vm_range_t *range) {
+ self->root = _vm_range__add(self->root, range);
+}
+
+
+void
+vm_range_tree__destroy(vm_range_tree_t *self) {
+ if (!isvalid(self))
+ return;
+
+ vm_range__destroy(self->root);
+
+ free(self);
+}
+
+#endif // PY_PROC_C
+
+#ifdef PY_THREAD_C
+
+static inline vm_range_t *
+_vm_range__find(vm_range_t *self, addr_t addr) {
+ if (!isvalid(self))
+ return NULL;
+
+ if (addr >= self->lo && addr < self->hi)
+ return self;
+
+ return _vm_range__find(addr < self->lo ? self->left : self->right, addr);
+}
+
+
+/**
+ * Query the tree for the range that contains the given address (if any).
+ *
+ * If any of the ranges stored within the VM range tree data structure overlap,
+ * the result of this method might be meaningless.
+ *
+ * @param self the VM range tree to query.
+ * @param addr the address to look up.
+ *
+ * @return a valid reference to a VM range, NULL otherwise.
+ */
+vm_range_t *
+vm_range_tree__find(vm_range_tree_t *self, addr_t addr) {
+ return _vm_range__find(self->root, addr);
+}
+
+#endif // PY_THREAD_C
+
+#endif
diff --git a/src/logging.h b/src/logging.h
index 06481c37..7bab717d 100644
--- a/src/logging.h
+++ b/src/logging.h
@@ -40,6 +40,15 @@
fprintf(pargs.output_file, __VA_ARGS__); \
NL;
+#ifdef NATIVE
+#define log_header() { \
+ log_m("\033[1m _ _ \033[0m"); \
+ log_m("\033[1m __ _ _ _ __| |_(_)_ _ \033[0m"); \
+ log_m("\033[1m/ _` | || (_-< _| | ' \\ \033[0m"); \
+ log_m("\033[1m\\__,_|\\_,_/__/\\__|_|_||_|\033[0m\033[31;1mp\033[0m \033[36;1m%s\033[0m", VERSION); \
+ log_i("====[ AUSTINP ]===="); \
+}
+#else
#define log_header() { \
log_m("\033[1m _ _ \033[0m"); \
log_m("\033[1m __ _ _ _ __| |_(_)_ _ \033[0m"); \
@@ -47,6 +56,7 @@
log_m("\033[1m\\__,_|\\_,_/__/\\__|_|_||_|\033[0m \033[36;1m%s\033[0m", VERSION); \
log_i("====[ AUSTIN ]===="); \
}
+#endif
#define log_footer() {}
/**
diff --git a/src/mac/py_proc.h b/src/mac/py_proc.h
index 9e04bc59..d078a3b7 100644
--- a/src/mac/py_proc.h
+++ b/src/mac/py_proc.h
@@ -36,6 +36,7 @@
#include
#include
+#include "../hints.h"
#define CHECK_HEAP
#define DEREF_SYM
@@ -416,13 +417,15 @@ _py_proc__get_maps(py_proc_t * self) {
if (!isvalid(path))
FAIL;
+ with_resources;
+
// NOTE: Mac OS X kernel bug. This also gives time to the VM maps to
// stabilise.
usleep(50000);
self->extra->task_id = pid_to_task(self->pid);
if (self->extra->task_id == 0)
- FAIL;
+ NOK;
self->min_raddr = (void *) -1;
self->max_raddr = NULL;
@@ -490,9 +493,12 @@ _py_proc__get_maps(py_proc_t * self) {
log_d("BSS bounds [%p - %p]", self->map.bss.base, self->map.bss.base + self->map.bss.size);
log_d("HEAP bounds [%p - %p]", self->map.heap.base, self->map.heap.base + self->map.heap.size);
+ retval = !self->sym_loaded;
+
+release:
free(path);
- return !self->sym_loaded;
+ released;
} // _py_proc__get_maps
diff --git a/src/mem.h b/src/mem.h
index 38818e90..10f72c77 100644
--- a/src/mem.h
+++ b/src/mem.h
@@ -103,7 +103,7 @@ typedef struct {
*/
static inline int
copy_memory(pid_t pid, void * addr, ssize_t len, void * buf) {
- ssize_t result;
+ ssize_t result = -1;
#if defined(PL_LINUX) /* LINUX */
struct iovec local[1];
diff --git a/src/platform.c b/src/platform.c
index ea281091..83305f31 100644
--- a/src/platform.c
+++ b/src/platform.c
@@ -4,15 +4,21 @@
#include "platform.h"
+#if defined PL_LINUX
+static size_t max_pid = 0;
+#endif
+
// ----------------------------------------------------------------------------
size_t
pid_max() {
#if defined PL_LINUX /* LINUX */
+ if (max_pid)
+ return max_pid;
+
FILE * pid_max_file = fopen("/proc/sys/kernel/pid_max", "rb");
if (!isvalid(pid_max_file))
return 0;
- size_t max_pid;
int has_pid_max = (fscanf(pid_max_file, "%ld", &max_pid) == 1);
fclose(pid_max_file);
if (!has_pid_max)
diff --git a/src/py_proc.c b/src/py_proc.c
index 5f6d1f25..64a774bc 100644
--- a/src/py_proc.c
+++ b/src/py_proc.c
@@ -40,13 +40,13 @@
#include "argparse.h"
#include "bin.h"
-#include "dict.h"
+#include "py_string.h"
#include "error.h"
#include "hints.h"
#include "logging.h"
#include "mem.h"
+#include "stack.h"
#include "stats.h"
-#include "version.h"
#include "py_proc.h"
#include "py_thread.h"
@@ -119,7 +119,7 @@ _py_proc__check_sym(py_proc_t * self, char * name, void * value) {
for (register int i = 0; i < DYNSYM_COUNT; i++) {
if (
- string_hash(name) == _dynsym_hash_array[i]
+ string__hash(name) == _dynsym_hash_array[i]
&&strcmp(name, _dynsym_array[i]) == 0
) {
*(&(self->tstate_curr_raddr) + i) = value;
@@ -225,9 +225,9 @@ _find_version_in_binary(char * path) {
static int
-_py_proc__get_version(py_proc_t * self) {
+_py_proc__infer_python_version(py_proc_t * self) {
if (self == NULL || (self->bin_path == NULL && self->lib_path == NULL))
- return NOVERSION;
+ FAIL;
int major = 0, minor = 0, patch = 0;
@@ -260,7 +260,8 @@ _py_proc__get_version(py_proc_t * self) {
}
base += needle_len;
if (sscanf(base,"%d.%d", &major, &minor) == 2) {
- return PYVERSION(major, minor, patch) | 0xFF;
+ self->py_v = get_version_descriptor(major, minor, patch);
+ SUCCESS;
}
}
@@ -274,13 +275,15 @@ _py_proc__get_version(py_proc_t * self) {
char * ver_needle = strstr(self->lib_path, "/3.");
if (ver_needle == NULL) ver_needle = strstr(self->lib_path, "/2.");
if (ver_needle == NULL || sscanf(ver_needle, "/%d.%d", &major, &minor) == 2) {
- return PYVERSION(major, minor, patch) | 0xFF;
+ self->py_v = get_version_descriptor(major, minor, patch);
+ SUCCESS;
}
// Still no version detected so we look into the binary content
int version = NOVERSION;
if (isvalid(self->lib_path) && (version = _find_version_in_binary(self->lib_path))) {
- return version;
+ self->py_v = get_version_descriptor(MAJOR(version), MINOR(version), PATCH(version));
+ SUCCESS;
}
#endif
}
@@ -291,16 +294,18 @@ _py_proc__get_version(py_proc_t * self) {
// content for clues
int version = NOVERSION;
if (isvalid(self->bin_path) && (version = _find_version_in_binary(self->bin_path))) {
- return version;
+ self->py_v = get_version_descriptor(MAJOR(version), MINOR(version), PATCH(version));
+ SUCCESS;
}
}
#endif
set_error(ENOVERSION);
- return NOVERSION;
+ FAIL;
from_exe:
- return PYVERSION(major, minor, patch);
+ self->py_v = get_version_descriptor(major, minor, patch);
+ SUCCESS;
// Scan the rodata section for something that looks like the Python version.
// There are good chances this is at the very beginning of the section so
@@ -331,6 +336,11 @@ _py_proc__get_version(py_proc_t * self) {
// ----------------------------------------------------------------------------
static int
_py_proc__check_interp_state(py_proc_t * self, void * raddr) {
+ if (!isvalid(self))
+ FAIL;
+
+ V_DESC(self->py_v);
+
PyInterpreterState is;
PyThreadState tstate_head;
@@ -360,18 +370,14 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
raddr, V_FIELD(void *, is, py_is, o_tstate_head)
);
- // As an extra sanity check, verify that the thread state is valid
- // 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");
- // FAIL;
- // }
+ #if defined PL_LINUX
+ raddr_t thread_raddr = {PROC_REF, V_FIELD(void *, is, py_is, o_tstate_head)};
+ py_thread_t thread;
- // if (thread.invalid) {
- // log_d("... but Head Thread State is invalid!");
- // FAIL;
- // }
+ if (fail(py_thread__fill_from_raddr(&thread, &thread_raddr, self))) {
+ log_d("Failed to fill thread structure");
+ FAIL;
+ }
log_d("Stack trace constructed from possible interpreter state");
@@ -380,6 +386,18 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) {
log_d("GC runtime state @ %p", self->gc_state_raddr);
}
+ // Try to determine the TID by reading the remote struct pthread structure.
+ // We can then use this information to parse the appropriate procfs file and
+ // determine the native thread's running state.
+ while (isvalid(thread.raddr.addr)) {
+ if (success(_infer_tid_field_offset(&thread)))
+ SUCCESS;
+ py_thread__next(&thread);
+ }
+ log_d("tid field offset not ready");
+ FAIL;
+ #endif
+
SUCCESS;
}
@@ -482,6 +500,11 @@ _py_proc__scan_bss(py_proc_t * self) {
// ----------------------------------------------------------------------------
static int
_py_proc__deref_interp_head(py_proc_t * self) {
+ if (!isvalid(self))
+ FAIL;
+
+ V_DESC(self->py_v);
+
void * interp_head_raddr;
if (self->py_runtime_raddr != NULL) {
@@ -546,6 +569,11 @@ _py_proc__get_current_thread_state_raddr(py_proc_t * self) {
// ----------------------------------------------------------------------------
static int
_py_proc__find_interpreter_state(py_proc_t * self) {
+ if (!isvalid(self))
+ FAIL;
+
+ V_DESC(self->py_v);
+
PyThreadState tstate_current;
void * tstate_current_raddr;
@@ -610,6 +638,13 @@ _py_proc__wait_for_interp_state(py_proc_t * self) {
#ifdef DEREF_SYM
if (fail(_py_proc__find_interpreter_state(self))) {
#endif
+ if (is_fatal(austin_errno)) {
+ log_d(
+ "Terminatig _py_proc__wait_for_interp_state loop because of fatal error code %d",
+ austin_errno
+ );
+ FAIL;
+ }
if (self->bss == NULL) {
self->bss = malloc(self->map.bss.size);
}
@@ -673,7 +708,10 @@ _py_proc__wait_for_interp_state(py_proc_t * self) {
// ----------------------------------------------------------------------------
static int
-_py_proc__run(py_proc_t * self, int try_once) {
+_py_proc__run(py_proc_t * self) {
+ austin_errno = EOK;
+
+ int try_once = self->child;
#ifdef DEBUG
if (try_once == FALSE)
log_d("Start up timeout: %d ms", pargs.timeout / 1000);
@@ -742,13 +780,9 @@ _py_proc__run(py_proc_t * self, int try_once) {
#endif
// Determine and set version
- if (!self->version) {
- if (!(self->version = _py_proc__get_version(self))) {
- set_error(ENOVERSION);
- FAIL;
- }
-
- set_version(self->version);
+ if (fail(_py_proc__infer_python_version(self))) {
+ set_error(ENOVERSION);
+ FAIL;
}
if (_py_proc__wait_for_interp_state(self))
@@ -767,22 +801,30 @@ _py_proc__run(py_proc_t * self, int try_once) {
// ----------------------------------------------------------------------------
py_proc_t *
-py_proc_new() {
+py_proc_new(int child) {
py_proc_t * py_proc = (py_proc_t *) calloc(1, sizeof(py_proc_t));
if (!isvalid(py_proc))
return NULL;
+ py_proc->child = child;
py_proc->min_raddr = (void *) -1;
py_proc->gc_state_raddr = NULL;
// Pre-hash symbol names
if (_dynsym_hash_array[0] == 0) {
for (register int i = 0; i < DYNSYM_COUNT; i++) {
- _dynsym_hash_array[i] = string_hash((char *) _dynsym_array[i]);
+ _dynsym_hash_array[i] = string__hash((char *) _dynsym_array[i]);
}
}
- py_proc->frames_heap.newlo = py_proc->frames.newlo = (void *) -1;
+ py_proc->frames_heap = py_proc->frames = NULL_MEM_BLOCK;
+
+ py_proc->frame_cache = lru_cache_new(MAX_STACK_SIZE, (void (*)(value_t)) frame__destroy);
+
+ if (!isvalid(py_proc->frame_cache)) {
+ log_e("Failed to allocate code object cache");
+ goto error;
+ }
py_proc->extra = (proc_extra_info *) calloc(1, sizeof(proc_extra_info));
if (!isvalid(py_proc->extra))
@@ -798,7 +840,7 @@ py_proc_new() {
// ----------------------------------------------------------------------------
int
-py_proc__attach(py_proc_t * self, pid_t pid, int child_process) {
+py_proc__attach(py_proc_t * self, pid_t pid) {
log_d("Attaching to process with PID %d", pid);
#if defined PL_WIN /* WIN */
@@ -813,7 +855,7 @@ py_proc__attach(py_proc_t * self, pid_t pid, int child_process) {
self->pid = pid;
- if (fail(_py_proc__run(self, child_process))) {
+ if (fail(_py_proc__run(self))) {
#if defined PL_WIN
if (fail(_py_proc__try_child_proc(self))) {
#endif
@@ -952,7 +994,7 @@ py_proc__start(py_proc_t * self, const char * exec, char * argv[]) {
log_d("New process created with PID %d", self->pid);
- if (fail(_py_proc__run(self, FALSE))) {
+ if (fail(_py_proc__run(self))) {
#if defined PL_WIN
if (fail(_py_proc__try_child_proc(self))) {
#endif
@@ -1005,6 +1047,8 @@ _py_proc__find_current_thread_offset(py_proc_t * self, void * thread_raddr) {
if (self->py_runtime_raddr == NULL)
FAIL;
+ V_DESC(self->py_v);
+
void * interp_head_raddr;
_PyRuntimeState py_runtime;
@@ -1081,6 +1125,8 @@ py_proc__is_gc_collecting(py_proc_t * self) {
if (!isvalid(self->gc_state_raddr))
return FALSE;
+ V_DESC(self->py_v);
+
GCRuntimeState gc_state;
if (fail(py_proc__get_type(self, self->gc_state_raddr, gc_state))) {
log_d("Failed to get GC runtime state");
@@ -1106,10 +1152,16 @@ _py_proc__interrupt_threads(py_proc_t * self, raddr_t * tstate_head_raddr) {
FAIL;
if (pargs.kernel && fail(py_thread__save_kernel_stack(&py_thread)))
FAIL;
- if (ptrace(PTRACE_INTERRUPT, py_thread.tid, 0, 0)) {
+ if (fail(wait_ptrace(PTRACE_INTERRUPT, py_thread.tid, 0, 0))) {
log_e("ptrace: failed to interrupt thread %d", py_thread.tid);
FAIL;
}
+ if (fail(py_thread__set_interrupted(&py_thread, TRUE))) {
+ if (fail(wait_ptrace(PTRACE_CONT, py_thread.tid, 0, 0))) {
+ log_d("ptrace: failed to resume interrupted thread %d (errno: %d)", py_thread.tid, errno);
+ }
+ FAIL;
+ }
log_t("ptrace: thread %d interrupted", py_thread.tid);
} while (success(py_thread__next(&py_thread)));
@@ -1127,10 +1179,17 @@ _py_proc__resume_threads(py_proc_t * self, raddr_t * tstate_head_raddr) {
}
do {
- while (ptrace(PTRACE_CONT, py_thread.tid, 0, 0)) {
- log_t("ptrace: failed to resume thread %d", py_thread.tid);
+ if (py_thread__is_interrupted(&py_thread)) {
+ if (fail(wait_ptrace(PTRACE_CONT, py_thread.tid, 0, 0))) {
+ log_d("ptrace: failed to resume thread %d (errno: %d)", py_thread.tid, errno);
+ FAIL;
+ }
+ log_t("ptrace: thread %d resumed", py_thread.tid);
+ if (fail(py_thread__set_interrupted(&py_thread, FALSE))) {
+ log_ie("Failed to mark thread as interrupted");
+ FAIL;
+ }
}
- log_t("ptrace: thread %d resumed", py_thread.tid);
} while (success(py_thread__next(&py_thread)));
SUCCESS;
@@ -1145,6 +1204,8 @@ py_proc__sample(py_proc_t * self) {
ssize_t mem_delta = 0;
void * current_thread = NULL;
+ V_DESC(self->py_v);
+
PyInterpreterState is;
if (fail(py_proc__get_type(self, self->is_raddr, is)))
FAIL;
@@ -1155,7 +1216,10 @@ py_proc__sample(py_proc_t * self) {
py_thread_t py_thread;
#ifdef NATIVE
- _py_proc__interrupt_threads(self, &raddr);
+ if (fail(_py_proc__interrupt_threads(self, &raddr))) {
+ log_ie("Failed to interrupt threads");
+ FAIL;
+ }
time_delta = gettime() - self->timestamp;
#endif
@@ -1194,7 +1258,10 @@ py_proc__sample(py_proc_t * self) {
} while (success(py_thread__next(&py_thread)));
#ifdef NATIVE
self->timestamp = gettime();
- _py_proc__resume_threads(self, &raddr);
+ if (fail(_py_proc__resume_threads(self, &raddr))) {
+ log_ie("Failed to resume threads");
+ FAIL;
+ }
#endif
}
@@ -1210,9 +1277,10 @@ py_proc__sample(py_proc_t * self) {
// ----------------------------------------------------------------------------
void
py_proc__log_version(py_proc_t * self, int parent) {
- int major = MAJOR(self->version);
- int minor = MINOR(self->version);
- int patch = PATCH(self->version);
+ int major = self->py_v->major;
+ int minor = self->py_v->minor;
+ int patch = self->py_v->patch;
+
if (pargs.pipe) {
if (patch == 0xFF) {
if (parent) {
@@ -1258,10 +1326,18 @@ py_proc__destroy(py_proc_t * self) {
if (!isvalid(self))
return;
+ #ifdef NATIVE
+ unw_destroy_addr_space(self->unwind.as);
+ vm_range_tree__destroy(self->maps_tree);
+ hash_table__destroy(self->base_table);
+ #endif
+
sfree(self->bin_path);
sfree(self->lib_path);
sfree(self->bss);
sfree(self->extra);
+ lru_cache__destroy(self->frame_cache);
+
free(self);
}
diff --git a/src/py_proc.h b/src/py_proc.h
index 8091a3ad..51cac228 100644
--- a/src/py_proc.h
+++ b/src/py_proc.h
@@ -27,12 +27,15 @@
#include
#ifdef NATIVE
-#include
#include
+#include "linux/vm-range-tree.h"
+#include "cache.h"
#endif
+#include "cache.h"
#include "heap.h"
#include "stats.h"
+#include "version.h"
typedef struct {
@@ -53,6 +56,7 @@ typedef struct _proc_extra_info proc_extra_info; // Forward declaration.
typedef struct {
pid_t pid;
+ int child;
char * bin_path;
char * lib_path;
@@ -64,7 +68,7 @@ typedef struct {
void * bss; // local copy of the remote bss section
int sym_loaded;
- int version;
+ python_v * py_v;
// Symbols from .dynsym
void * tstate_curr_raddr;
@@ -74,6 +78,8 @@ typedef struct {
void * is_raddr;
+ lru_cache_t * frame_cache;
+
// Temporal profiling support
ctime_t timestamp;
@@ -89,8 +95,10 @@ typedef struct {
#ifdef NATIVE
struct _puw {
- unw_addr_space_t as;
- } unwind;
+ unw_addr_space_t as;
+ } unwind;
+ vm_range_tree_t * maps_tree;
+ hash_table_t * base_table;
#endif
// Platform-dependent fields
@@ -101,11 +109,13 @@ typedef struct {
/**
* Create a new process object. Use it to start the process that needs to be
* sampled from austin.
+ *
+ * @param child whether this is a child process.
*
- * @return a pointer to the newly created py_proc_t object.
+ * @return a pointer to the newly created py_proc_t object.
*/
py_proc_t *
-py_proc_new(void);
+py_proc_new(int child);
/**
@@ -126,12 +136,11 @@ py_proc__start(py_proc_t *, const char *, char **);
*
* @param py_proc_t * the process object.
* @param pid_t the PID of the process to attach.
- * @param int TRUE if this is a child process, FALSE otherwise.
*
* @return 0 on success.
*/
int
-py_proc__attach(py_proc_t *, pid_t, int);
+py_proc__attach(py_proc_t *, pid_t);
/**
@@ -184,7 +193,7 @@ py_proc__is_gc_collecting(py_proc_t *);
*
* @param py_proc_t * self.
- * @return 0 if the sampling succeded; 1 otherwise.
+ * @return 0 if the sampling succeeded; 1 otherwise.
*/
int
py_proc__sample(py_proc_t *);
diff --git a/src/py_proc_list.c b/src/py_proc_list.c
index ffef7a45..642a8c6d 100644
--- a/src/py_proc_list.c
+++ b/src/py_proc_list.c
@@ -110,7 +110,7 @@ _py_proc_list__remove(py_proc_list_t * self, py_proc_item_t * item) {
py_proc_list_t *
py_proc_list_new(py_proc_t * parent_py_proc) {
py_proc_list_t * list = (py_proc_list_t *) calloc(1, sizeof(py_proc_list_t));
- if (list == NULL)
+ if (!isvalid(list))
return NULL;
list->pids = pid_max();
@@ -119,18 +119,22 @@ py_proc_list_new(py_proc_t * parent_py_proc) {
list->index = (py_proc_t **) calloc(list->pids, sizeof(py_proc_t *));
if (list->index == NULL)
- return NULL;
+ goto release;
list->pid_table = (pid_t *) calloc(list->pids, sizeof(pid_t));
if (list->pid_table == NULL) {
free(list->index);
- return NULL;
+ goto release;
}
// Add the parent process to the list.
_py_proc_list__add(list, parent_py_proc);
return list;
+
+release:
+ py_proc_list__destroy(list);
+ return NULL;
} /* py_proc_list_new */
@@ -139,11 +143,11 @@ void
py_proc_list__add_proc_children(py_proc_list_t * self, pid_t ppid) {
for (register pid_t pid = 0; pid <= self->max_pid; pid++) {
if (self->pid_table[pid] == ppid && !_py_proc_list__has_pid(self, pid)) {
- py_proc_t * child_proc = py_proc_new();
+ py_proc_t * child_proc = py_proc_new(TRUE);
if (child_proc == NULL)
continue;
- if (py_proc__attach(child_proc, pid, TRUE)) {
+ if (py_proc__attach(child_proc, pid)) {
py_proc__destroy(child_proc);
continue;
}
@@ -168,10 +172,17 @@ void
py_proc_list__sample(py_proc_list_t * self) {
log_t("Sampling from process list");
- for (py_proc_item_t * item = self->first; item != NULL; item = item->next) {
+ 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();
- py_proc__sample(item->py_proc); // Fail silently
+ if (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);
+ item = next;
+ }
+ else
+ item = item->next;
stopwatch_duration();
}
} /* py_proc_list__sample */
diff --git a/src/py_string.h b/src/py_string.h
new file mode 100644
index 00000000..7914d0e8
--- /dev/null
+++ b/src/py_string.h
@@ -0,0 +1,181 @@
+// 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) 2018-2022 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 .
+
+#ifndef PY_STRING_H
+#define PY_STRING_H
+
+#include
+#include
+
+#include "hints.h"
+#include "logging.h"
+#include "mem.h"
+#include "python/string.h"
+#include "version.h"
+
+#define MAGIC_TINY 7
+#define MAGIC_BIG 1000003
+#define p_ascii_data(raddr) (raddr + sizeof(PyASCIIObject))
+
+
+// ----------------------------------------------------------------------------
+static inline long
+string__hash(char * string) {
+ // Stolen from stringobject.c
+ register unsigned char *p;
+ register long x;
+
+ p = (unsigned char *) string;
+ x = *p << MAGIC_TINY;
+ while (*p != 0)
+ x = (MAGIC_BIG * x) ^ *(p++);
+ x ^= strlen(string);
+ return x == 0 ? 1 : x;
+}
+
+
+// ----------------------------------------------------------------------------
+static inline char *
+_string_from_raddr(pid_t pid, void * raddr, python_v * py_v) {
+ PyStringObject string;
+ PyUnicodeObject3 unicode;
+ char * buffer = NULL;
+ ssize_t len = 0;
+
+ // This switch statement is required by the changes regarding the string type
+ // introduced in Python 3.
+ switch (py_v->major) {
+ case 2:
+ if (fail(copy_datatype(pid, raddr, string))) {
+ log_ie("Cannot read remote PyStringObject");
+ goto failed;
+ }
+
+ len = string.ob_base.ob_size;
+ buffer = (char *) malloc(len + 1);
+ if (fail(copy_memory(pid, raddr + offsetof(PyStringObject, ob_sval), len, buffer))) {
+ log_ie("Cannot read remote value of PyStringObject");
+ goto failed;
+ }
+ buffer[len] = 0;
+ break;
+
+ case 3:
+ if (fail(copy_datatype(pid, raddr, unicode))) {
+ log_ie("Cannot read remote PyUnicodeObject3");
+ goto failed;
+ }
+
+ PyASCIIObject ascii = unicode._base._base;
+
+ if (ascii.state.kind != 1) {
+ set_error(ECODEFMT);
+ goto failed;
+ }
+
+ void * data = ascii.state.compact ? p_ascii_data(raddr) : unicode._base.utf8;
+ len = ascii.state.compact ? ascii.length : unicode._base.utf8_length;
+
+ if (len < 0 || len > 4096) {
+ log_e("Invalid string length");
+ goto failed;
+ }
+
+ buffer = (char *) malloc(len + 1);
+
+ if (!isvalid(data) || fail(copy_memory(pid, data, len, buffer))) {
+ log_ie("Cannot read remote value of PyUnicodeObject3");
+ goto failed;
+ }
+ buffer[len] = 0;
+ }
+
+ return buffer;
+
+failed:
+ sfree(buffer);
+ return NULL;
+}
+
+
+// ----------------------------------------------------------------------------
+static inline unsigned char *
+_bytes_from_raddr(pid_t pid, void * raddr, ssize_t * size, python_v * py_v) {
+ PyStringObject string;
+ PyBytesObject bytes;
+ ssize_t len = 0;
+ unsigned char * array = NULL;
+
+ switch (py_v->major) {
+ case 2: // Python 2
+ if (fail(copy_datatype(pid, raddr, string))) {
+ log_ie("Cannot read remote PyStringObject");
+ goto error;
+ }
+
+ len = string.ob_base.ob_size + 1;
+ if (py_v->minor <= 4) {
+ // In Python 2.4, the ob_size field is of type int. If we cannot
+ // allocate on the first try it's because we are getting a ridiculous
+ // value for len. In that case, chop it down to an int and try again.
+ // This approach is simpler than adding version support.
+ len = (int) len;
+ }
+
+ array = (unsigned char *) malloc((len + 1) * sizeof(unsigned char *));
+ if (fail(copy_memory(pid, raddr + offsetof(PyStringObject, ob_sval), len, array))) {
+ log_ie("Cannot read remote value of PyStringObject");
+ goto error;
+ }
+ break;
+
+ case 3: // Python 3
+ if (fail(copy_datatype(pid, raddr, bytes))) {
+ log_ie("Cannot read remote PyBytesObject");
+ goto error;
+ }
+
+ if ((len = bytes.ob_base.ob_size + 1) < 1) { // Include null-terminator
+ set_error(ECODEBYTES);
+ log_e("PyBytesObject is too short");
+ goto error;
+ }
+
+ array = (unsigned char *) malloc((len + 1) * sizeof(unsigned char *));
+ if (fail(copy_memory(pid, raddr + offsetof(PyBytesObject, ob_sval), len, array))) {
+ log_ie("Cannot read remote value of PyBytesObject");
+ goto error;
+ }
+ }
+
+ array[len] = 0;
+ *size = len - 1;
+
+ return array;
+
+error:
+ sfree(array);
+ return NULL;
+}
+
+
+#endif
diff --git a/src/py_thread.c b/src/py_thread.c
index 338ca285..d93bdc9c 100644
--- a/src/py_thread.c
+++ b/src/py_thread.c
@@ -29,11 +29,13 @@
#include
#include "argparse.h"
+#include "cache.h"
#include "error.h"
#include "hints.h"
#include "logging.h"
#include "mem.h"
#include "platform.h"
+#include "stack.h"
#include "timing.h"
#include "version.h"
@@ -47,6 +49,9 @@
#if defined(PL_LINUX)
#include "linux/py_thread.h"
+ #if defined NATIVE && defined HAVE_BFD
+ #include "linux/addr2line.h"
+ #endif
#elif defined(PL_WIN)
@@ -61,252 +66,34 @@
// ---- PRIVATE ---------------------------------------------------------------
-#define MAX_STACK_SIZE 4096
-#define MAXLEN 1024
-
-
-typedef struct {
- char filename [MAXLEN];
- char scope [MAXLEN];
- unsigned int lineno;
-} py_code_t;
-
-
-typedef struct frame {
- raddr_t raddr;
- raddr_t prev_raddr;
+#define NULL_HEAP ((_heap_t) {NULL, 0})
- py_code_t code;
-
- int invalid; // Set when prev_radd != null but unable to copy.
-} py_frame_t;
-
-
-static py_frame_t * _stack = NULL;
-static size_t _stackp = 0;
-static _heap_t _frames = {NULL, 0};
-static _heap_t _frames_heap = {NULL, 0};
+static _heap_t _frames = NULL_HEAP;
+static _heap_t _frames_heap = NULL_HEAP;
+static size_t max_pid = 0;
#ifdef NATIVE
static void ** _tids = NULL;
static unsigned char * _tids_idle = NULL;
+static unsigned char * _tids_int = NULL;
static char ** _kstacks = NULL;
#endif
+#if defined PL_WIN
+#define fprintfp _fprintf_p
+#else
+#define fprintfp fprintf
+#endif
-// ---- PyCode ----------------------------------------------------------------
-
-#define _code__get_filename(self, pid, dest) _get_string_from_raddr(pid, *((void **) ((void *) self + py_v->py_code.o_filename)), dest)
-#define _code__get_name(self, pid, dest) _get_string_from_raddr(pid, *((void **) ((void *) self + py_v->py_code.o_name)), dest)
-
-#define _code__get_lnotab(self, pid, buf) _get_bytes_from_raddr(pid, *((void **) ((void *) self + py_v->py_code.o_lnotab)), buf)
-
-#define p_ascii_data(raddr) (raddr + sizeof(PyASCIIObject))
-
-
-
-// ----------------------------------------------------------------------------
-
-
-static inline int
-_get_string_from_raddr(pid_t pid, void * raddr, char * buffer) {
- PyStringObject string;
- PyUnicodeObject3 unicode;
-
- // This switch statement is required by the changes regarding the string type
- // introduced in Python 3.
- switch (py_v->major) {
- case 2:
- if (fail(copy_datatype(pid, raddr, string))) {
- log_ie("Cannot read remote PyStringObject");
- FAIL;
- }
-
- ssize_t len = string.ob_base.ob_size;
- if (len >= MAXLEN)
- len = MAXLEN-1;
- if (fail(copy_memory(pid, raddr + offsetof(PyStringObject, ob_sval), len, buffer))) {
- log_ie("Cannot read remote value of PyStringObject");
- FAIL;
- }
- buffer[len] = 0;
- break;
-
- case 3:
- if (fail(copy_datatype(pid, raddr, unicode))) {
- log_ie("Cannot read remote PyUnicodeObject3");
- FAIL;
- }
- if (unicode._base._base.state.kind != 1) {
- set_error(ECODEFMT);
- FAIL;
- }
- if (unicode._base._base.state.compact != 1) {
- set_error(ECODECMPT);
- FAIL;
- }
-
- len = unicode._base._base.length;
- if (len >= MAXLEN)
- len = MAXLEN-1;
-
- if (fail(copy_memory(pid, p_ascii_data(raddr), len, buffer))) {
- log_ie("Cannot read remote value of PyUnicodeObject3");
- FAIL;
- }
- buffer[len] = 0;
- }
-
- SUCCESS;
-}
-
-
-// ----------------------------------------------------------------------------
-static inline int
-_get_bytes_from_raddr(pid_t pid, void * raddr, unsigned char * array) {
- PyStringObject string;
- PyBytesObject bytes;
- ssize_t len = 0;
-
- if (!isvalid(array))
- goto error;
-
- switch (py_v->major) {
- case 2: // Python 2
- if (fail(copy_datatype(pid, raddr, string))) {
- log_ie("Cannot read remote PyStringObject");
- goto error;
- }
-
- len = string.ob_base.ob_size + 1;
- if (len >= MAXLEN) {
- // In Python 2.4, the ob_size field is of type int. If we cannot
- // allocate on the first try it's because we are getting a ridiculous
- // value for len. In that case, chop it down to an int and try again.
- // This approach is simpler than adding version support.
- len = (int) len;
- if (len >= MAXLEN) {
- log_w("Using MAXLEN when retrieving Bytes object.");
- len = MAXLEN-1;
- }
- }
-
- if (fail(copy_memory(pid, raddr + offsetof(PyStringObject, ob_sval), len, array))) {
- log_ie("Cannot read remote value of PyStringObject");
- len = 0;
- goto error;
- }
- break;
-
- case 3: // Python 3
- if (fail(copy_datatype(pid, raddr, bytes))) {
- log_ie("Cannot read remote PyBytesObject");
- goto error;
- }
-
- if ((len = bytes.ob_base.ob_size + 1) < 1) { // Include null-terminator
- set_error(ECODEBYTES);
- goto error;
- }
-
- if (len >= MAXLEN) {
- log_w("Using MAXLEN when retrieving Bytes object.");
- len = MAXLEN-1;
- }
-
- if (fail(copy_memory(pid, raddr + offsetof(PyBytesObject, ob_sval), len, array))) {
- log_ie("Cannot read remote value of PyBytesObject");
- len = 0;
- goto error;
- }
- }
-
- array[len] = 0;
-
-error:
- return len - 1; // The last char is guaranteed to be the null terminator
-}
-
-
-// ----------------------------------------------------------------------------
-static inline int
-_py_code__fill_from_raddr(py_code_t * self, raddr_t * raddr, int lasti) {
- PyCodeObject code;
- unsigned char lnotab[MAXLEN];
- int len;
-
- if (self == NULL)
- FAIL;
-
- if (fail(copy_from_raddr_v(raddr, code, py_v->py_code.size))) {
- log_ie("Cannot read remote PyCodeObject");
- FAIL;
- }
-
- if (fail(_code__get_filename(&code, raddr->pid, self->filename))) {
- log_ie("Cannot get file name from PyCodeObject");
- FAIL;
- }
-
- if (fail(_code__get_name(&code, raddr->pid, self->scope))) {
- log_ie("Cannot get scope name from PyCodeObject");
- FAIL;
- }
-
- else if ((len = _code__get_lnotab(&code, raddr->pid, lnotab)) < 0 || len % 2) {
- log_ie("Cannot get line number from PyCodeObject");
- FAIL;
- }
-
- int lineno = V_FIELD(unsigned int, code, py_code, o_firstlineno);
-
- if (py_v->major == 3 && py_v->minor >= 10) { // Python >=3.10
- lasti <<= 1;
- for (register int i = 0, bc = 0; i < len; i++) {
- int sdelta = lnotab[i++];
- if (sdelta == 0xff)
- break;
-
- bc += sdelta;
-
- int ldelta = lnotab[i];
- if (ldelta == 0x80)
- ldelta = 0;
- else if (ldelta > 0x80)
- lineno -= 0x100;
-
- lineno += ldelta;
- if (bc > lasti)
- break;
- }
- }
- else { // Python < 3.10
- for (register int i = 0, bc = 0; i < len; i++) {
- bc += lnotab[i++];
- if (bc > lasti)
- break;
-
- if (lnotab[i] >= 0x80)
- lineno -= 0x100;
-
- lineno += lnotab[i];
- }
- }
-
- self->lineno = lineno;
-
- SUCCESS;
-}
-
-
-// ---- PyFrame ---------------------------------------------------------------
// ----------------------------------------------------------------------------
#define _use_heaps (pargs.heap > 0)
-#define _no_heaps {pargs.heap = 0;}
-static inline int
+static inline void
_py_thread__read_frames(py_thread_t * self) {
+ if (!pargs.heap)
+ return;
+
size_t newsize;
size_t maxsize = pargs.heap >> 1;
@@ -321,8 +108,12 @@ _py_thread__read_frames(py_thread_t * self) {
self->proc->frames.hi = self->proc->frames.newhi;
self->proc->frames.lo = self->proc->frames.newlo;
}
- if (fail(copy_memory(self->raddr.pid, self->proc->frames.lo, newsize, _frames.content)))
- FAIL;
+ if (fail(copy_memory(self->raddr.pid, self->proc->frames.lo, newsize, _frames.content))) {
+ log_d("Failed to read remote frame area; will reset");
+ sfree(_frames.content);
+ _frames = NULL_HEAP;
+ self->proc->frames = NULL_MEM_BLOCK;
+ }
}
if (isvalid(self->proc->frames_heap.newhi)) {
@@ -336,38 +127,82 @@ _py_thread__read_frames(py_thread_t * self) {
self->proc->frames_heap.hi = self->proc->frames_heap.newhi;
self->proc->frames_heap.lo = self->proc->frames_heap.newlo;
}
- return copy_memory(self->raddr.pid, self->proc->frames_heap.lo, newsize, _frames_heap.content);
+ if (fail(copy_memory(self->raddr.pid, self->proc->frames_heap.lo, newsize, _frames_heap.content))) {
+ log_d("Failed to read remote frame area near heap; will reset");
+ sfree(_frames_heap.content);
+ _frames_heap = NULL_HEAP;
+ self->proc->frames_heap = NULL_MEM_BLOCK;
+
+ }
}
- SUCCESS;
}
// ----------------------------------------------------------------------------
+#ifdef DEBUG
+static unsigned int _frame_cache_miss = 0;
+static unsigned int _frame_cache_total = 0;
+#endif
+
static inline int
-_py_frame_fill_from_addr(PyFrameObject * frame, raddr_t * raddr) {
- py_frame_t * self = _stack + _stackp;
- self->invalid = TRUE;
+_py_thread__resolve_py_stack(py_thread_t * self) {
+ lru_cache_t * cache = self->proc->frame_cache;
- raddr_t py_code_raddr = {
- .pid = raddr->pid,
- .addr = V_FIELD_PTR(void *, frame, py_frame, o_code)
- };
- if (_py_code__fill_from_raddr(
- &(self->code), &py_code_raddr, V_FIELD_PTR(int, frame, py_frame, o_lasti)
- )) {
- log_ie("Cannot get PyCodeObject for frame");
- FAIL;
+ for (int i = 0; i < stack_pointer(); i++) {
+ py_frame_t py_frame = stack_py_get(i);
+
+ int lasti = py_frame.lasti;
+ key_dt frame_key = ((key_dt) py_frame.code << 16) | lasti;
+ frame_t * frame = lru_cache__maybe_hit(cache, frame_key);
+
+ #ifdef DEBUG
+ _frame_cache_total++;
+ #endif
+
+ if (!isvalid(frame)) {
+ #ifdef DEBUG
+ _frame_cache_miss++;
+ #endif
+ frame = _frame_from_code_raddr(
+ &(raddr_t) {self->raddr.pid, py_frame.code}, lasti, self->proc->py_v
+ );
+ if (!isvalid(frame)) {
+ log_ie("Failed to get frame from code object");
+ // Truncate the stack to the point where we have successfully resolved.
+ _stack->pointer = i;
+ FAIL;
+ }
+ lru_cache__store(cache, frame_key, frame);
+ }
+
+ stack_set(i, frame);
}
- self->raddr.pid = raddr->pid;
- self->raddr.addr = raddr->addr;
+ SUCCESS;
+}
- self->prev_raddr.pid = raddr->pid;
- self->prev_raddr.addr = V_FIELD_PTR(void *, frame, py_frame, o_back);
- self->invalid = FALSE;
+// ----------------------------------------------------------------------------
+static inline int
+_py_thread__push_frame_from_addr(py_thread_t * self, PyFrameObject * frame_obj, void ** prev) {
+ if (!isvalid(self))
+ FAIL;
+
+ V_DESC(self->proc->py_v);
+
+ void * origin = *prev;
+
+ *prev = V_FIELD_PTR(void *, frame_obj, py_frame, o_back);
+ if (unlikely(origin == *prev)) {
+ log_d("Frame points to itself!");
+ FAIL;
+ }
- _stackp++;
+ stack_py_push(
+ origin,
+ V_FIELD_PTR(void *, frame_obj, py_frame, o_code),
+ V_FIELD_PTR(int, frame_obj, py_frame, o_lasti)
+ );
SUCCESS;
}
@@ -375,166 +210,158 @@ _py_frame_fill_from_addr(PyFrameObject * frame, raddr_t * raddr) {
// ----------------------------------------------------------------------------
static inline int
-_py_frame_fill_from_raddr(raddr_t * raddr) {
+_py_thread__push_frame_from_raddr(py_thread_t * self, void ** prev) {
PyFrameObject frame;
- if (fail(copy_from_raddr_v(raddr, frame, py_v->py_frame.size))) {
+ raddr_t raddr = {self->raddr.pid, *prev};
+ if (fail(copy_from_raddr_v((&raddr), frame, self->proc->py_v->py_frame.size))) {
log_ie("Cannot read remote PyFrameObject");
- log_d(" raddr: (%p, %ld)", raddr->addr, raddr->pid);
FAIL;
}
- return _py_frame_fill_from_addr(&frame, raddr);
+ return _py_thread__push_frame_from_addr(self, &frame, prev);
}
// ----------------------------------------------------------------------------
-#define REL(raddr, block, base) (raddr->addr - block.lo + base)
+#define REL(raddr, block, base) (raddr - block.lo + base)
+
+#ifdef DEBUG
+static unsigned int _frames_total = 0;
+static unsigned int _frames_miss = 0;
+#endif
static inline int
-_py_frame_fill(raddr_t * raddr, py_thread_t * thread) {
+_py_thread__push_frame(py_thread_t * self, void ** prev) {
+ void * raddr = *prev;
if (_use_heaps) {
- py_proc_t * proc = thread->proc;
+ #ifdef DEBUG
+ _frames_total++;
+ #endif
+ py_proc_t * proc = self->proc;
if (isvalid(_frames.content)
- && raddr->addr >= proc->frames.lo
- && raddr->addr < proc->frames.lo + _frames.size
+ && raddr >= proc->frames.lo
+ && raddr < proc->frames.lo + _frames.size
) {
- return _py_frame_fill_from_addr(
- REL(raddr, proc->frames, _frames.content),
- raddr
+ return _py_thread__push_frame_from_addr(
+ self, REL(raddr, proc->frames, _frames.content), prev
);
}
else if (isvalid(_frames_heap.content)
- && raddr->addr >= proc->frames_heap.lo
- && raddr->addr < proc->frames_heap.lo + _frames_heap.size
+ && raddr >= proc->frames_heap.lo
+ && raddr < proc->frames_heap.lo + _frames_heap.size
) {
- return _py_frame_fill_from_addr(
- REL(raddr, proc->frames_heap, _frames_heap.content),
- raddr
+ return _py_thread__push_frame_from_addr(
+ self, REL(raddr, proc->frames_heap, _frames_heap.content), prev
);
}
+ #ifdef DEBUG
+ _frames_miss++;
+ #endif
+
// Miss: update ranges
// We quite likely set the bss map data so this should be a pretty reliable
// platform-independent way of dualising the frame heap.
- if (raddr->addr >= proc->map.bss.base && raddr->addr <= proc->map.bss.base + (1 << 27)) {
- if (raddr->addr + sizeof(PyFrameObject) > proc->frames_heap.newhi) {
- proc->frames_heap.newhi = raddr->addr + sizeof(PyFrameObject);
+ if (raddr >= proc->map.bss.base && raddr <= proc->map.bss.base + (1 << 27)) {
+ if (raddr + sizeof(PyFrameObject) > proc->frames_heap.newhi) {
+ proc->frames_heap.newhi = raddr + sizeof(PyFrameObject);
}
- if (raddr->addr < proc->frames_heap.newlo) {
- proc->frames_heap.newlo = raddr->addr;
+ if (raddr < proc->frames_heap.newlo) {
+ proc->frames_heap.newlo = raddr;
}
}
else {
- if (raddr->addr + sizeof(PyFrameObject) > proc->frames.newhi) {
- proc->frames.newhi = raddr->addr + sizeof(PyFrameObject);
+ if (raddr + sizeof(PyFrameObject) > proc->frames.newhi) {
+ proc->frames.newhi = raddr + sizeof(PyFrameObject);
}
- if (raddr->addr < proc->frames.newlo) {
- proc->frames.newlo = raddr->addr;
+ if (raddr < proc->frames.newlo) {
+ proc->frames.newlo = raddr;
}
}
}
- return _py_frame_fill_from_raddr(raddr);
+ return _py_thread__push_frame_from_raddr(self, prev);
}
// ----------------------------------------------------------------------------
static inline int
-_py_frame__prev(py_thread_t * thread) {
- if (_stackp <= 0)
- FAIL;
-
- py_frame_t * self = _stack + _stackp - 1;
- if (!isvalid(self) || !isvalid(self->prev_raddr.addr)) {
- // Double-check it's the end of the stack if we're using the heap.
- _stackp--;
- if (fail(_py_frame_fill_from_raddr(&self->raddr)) || !isvalid(self->prev_raddr.addr)) {
- FAIL;
- }
- }
-
- raddr_t prev_raddr = {
- .pid = self->prev_raddr.pid,
- .addr = self->prev_raddr.addr
- };
+_py_thread__unwind_frame_stack(py_thread_t * self) {
+ int invalid = FALSE;
- int result = _py_frame_fill(&prev_raddr, thread);
+ _py_thread__read_frames(self);
+
+ stack_reset();
- if (!_use_heaps) {
- return result;
+ void * prev = self->top_frame;
+ if (fail(_py_thread__push_frame(self, &prev))) {
+ log_ie("Failed to fill top frame");
+ FAIL;
}
- // This sucks! :(
- py_frame_t * last = self + 1;
- for (py_frame_t * f = self; f >= _stack; f--) {
- if (last->prev_raddr.addr == f->raddr.addr) {
+ while (isvalid(prev)) {
+ if (fail(_py_thread__push_frame(self, &prev))) {
+ log_d("Failed to retrieve frame #%d (from top).", stack_pointer());
+ invalid = TRUE;
+ break;
+ }
+ if (stack_full()) {
+ log_w("Invalid frame stack: too tall");
+ invalid = TRUE;
+ break;
+ }
+ if (stack_has_cycle()) {
log_d("Circular frame reference detected");
- last->invalid = TRUE;
- FAIL;
+ invalid = TRUE;
+ break;
}
}
+
+ invalid = fail(_py_thread__resolve_py_stack(self)) || invalid;
- return result;
+ return invalid;
}
+#ifdef NATIVE
// ----------------------------------------------------------------------------
-static inline int
-_py_thread__unwind_frame_stack(py_thread_t * self) {
- size_t basep = _stackp;
-
- if (_use_heaps && fail(_py_thread__read_frames(self))) {
- log_ie("Failed to read frames heaps");
- _no_heaps;
- FAIL;
- }
- raddr_t frame_raddr = { .pid = self->raddr.pid, .addr = self->top_frame };
- if (fail(_py_frame_fill(&frame_raddr, self))) {
- log_ie("Failed to fill top frame");
- FAIL;
- }
+int
+py_thread__set_idle(py_thread_t * self) {
+ unsigned char bit = 1 << (self->tid & 7);
+ size_t index = self->tid >> 3;
- while (success(_py_frame__prev(self))) {
- if (_stackp >= MAX_STACK_SIZE) {
- log_w("Discarding frame stack: too tall");
- FAIL;
- }
- }
-
- if (_stack[_stackp-1].invalid) {
- log_d("Frame number %d is invalid", _stackp - basep);
- FAIL;
+ if (_py_thread__is_idle(self)) {
+ _tids_idle[index] |= bit;
+ } else {
+ _tids_idle[index] &= ~bit;
}
- self->stack_height += _stackp - basep;
-
SUCCESS;
}
-
-#ifdef NATIVE
// ----------------------------------------------------------------------------
int
-py_thread__set_idle(py_thread_t * self) {
- size_t index = self->tid >> 3;
- int offset = self->tid & 7;
-
- if (unlikely(_pthread_tid_offset == 0)) {
- FAIL;
- }
+py_thread__set_interrupted(py_thread_t * self, int state) {
+ unsigned char bit = 1 << (self->tid & 7);
+ size_t index = self->tid >> 3;
- unsigned char idle_bit = _py_thread__is_idle(self) << offset;
- if (idle_bit) {
- _tids_idle[index] |= idle_bit;
+ if (state) {
+ _tids_int[index] |= bit;
} else {
- _tids_idle[index] &= ~idle_bit;
+ _tids_int[index] &= ~bit;
}
SUCCESS;
}
+// ----------------------------------------------------------------------------
+int
+py_thread__is_interrupted(py_thread_t * self) {
+ return _tids_int[self->tid >> 3] & (1 << (self->tid & 7));
+}
+
// ----------------------------------------------------------------------------
#define MAX_STACK_FILE_SIZE 2048
int
@@ -542,9 +369,8 @@ py_thread__save_kernel_stack(py_thread_t * self) {
char stack_path[48];
int fd;
- if (unlikely(_pthread_tid_offset == 0) || !isvalid(_kstacks) ) {
+ if (!isvalid(_kstacks))
FAIL;
- }
sfree(_kstacks[self->tid]);
@@ -555,7 +381,7 @@ py_thread__save_kernel_stack(py_thread_t * self) {
_kstacks[self->tid] = (char *) calloc(1, MAX_STACK_FILE_SIZE);
if (read(fd, _kstacks[self->tid], MAX_STACK_FILE_SIZE) == -1) {
- log_e("stack: filed to read %s", stack_path);
+ log_e("stack: failed to read %s", stack_path);
close(fd);
FAIL;
};
@@ -573,6 +399,8 @@ _py_thread__unwind_kernel_frame_stack(py_thread_t * self) {
log_t("linux: unwinding kernel stack");
+ stack_kernel_reset();
+
for (;;) {
char * eol = strchr(line, '\n');
if (!isvalid(eol))
@@ -584,9 +412,8 @@ _py_thread__unwind_kernel_frame_stack(py_thread_t * self) {
char * e = strchr(++b, '+');
if (isvalid(e))
*e = 0;
- strcpy(_stack[_stackp].code.scope, ++b);
- strcpy(_stack[_stackp].code.filename, "kernel");
- _stackp++; // TODO: Decide whether to decremet this by 2 before returning.
+
+ stack_kernel_push(strdup(++b));
}
line = eol + 1;
}
@@ -596,36 +423,101 @@ _py_thread__unwind_kernel_frame_stack(py_thread_t * self) {
// ----------------------------------------------------------------------------
+static char _native_buf[MAXLEN];
+
+static inline int
+wait_unw_init_remote(unw_cursor_t * c, unw_addr_space_t as, void * arg) {
+ int outcome = 0;
+ ctime_t end = gettime() + 1000;
+ while(gettime() <= end && (outcome = unw_init_remote(c, as, arg)) == -UNW_EBADREG)
+ sched_yield();
+ if (fail(outcome))
+ log_e("unwind: failed to initialize cursor (%d)", outcome);
+ return outcome;
+}
+
static inline int
_py_thread__unwind_native_frame_stack(py_thread_t * self) {
- void *context = _tids[self->tid];
unw_cursor_t cursor;
unw_word_t offset, pc;
- if (unw_init_remote(&cursor, self->proc->unwind.as, context))
+ lru_cache_t * cache = self->proc->frame_cache;
+ void * context = _tids[self->tid];
+
+ if (!isvalid(context)) {
+ log_e("libunwind: unexpected invalid context");
+ FAIL;
+ }
+
+ if (fail(wait_unw_init_remote(&cursor, self->proc->unwind.as, context)))
FAIL;
+ stack_native_reset();
+
do {
if (unw_get_reg(&cursor, UNW_REG_IP, &pc)) {
log_e("libunwind: cannot read program counter\n");
FAIL;
}
- if (unw_get_proc_name(&cursor, _stack[_stackp].code.scope, MAXLEN, &offset) == 0) {
- // To retrieve source name and line number we would need to
- // - resolve the PC to a map to get the binary path
- // - use the offset with the binary to get the line number from DWARF (see
- // https://kernel.googlesource.com/pub/scm/linux/kernel/git/hjl/binutils/+/hjl/secondary/binutils/addr2line.c)
- _stack[_stackp].code.lineno = offset;
- }
- else {
- strcpy(_stack[_stackp].code.scope, "");
- _stack[_stackp].code.lineno = 0;
+ #ifdef DEBUG
+ _frame_cache_total++;
+ #endif
+
+ frame_t * frame = lru_cache__maybe_hit(cache, pc);
+ if (!isvalid(frame)) {
+ #ifdef DEBUG
+ _frame_cache_miss++;
+ #endif
+ char * scope, * filename;
+ vm_range_t * range = NULL;
+ if (pargs.where) {
+ range = vm_range_tree__find(self->proc->maps_tree, pc);
+ // TODO: A failed attempt to find a range is an indication that we need
+ // to regenerate the VM maps. This would be of no use at the moment,
+ // since we only use them in `where` mode where we sample just once. If
+ // we resort to improving addr2line and use the VM range tree for
+ // normal mode, then we should consider catching the case
+ // !isvalid(range) and regenerate the VM range tree with fresh data.
+ #ifdef HAVE_BFD
+ if (isvalid(range)) {
+ unw_word_t base = (unw_word_t) hash_table__get(
+ self->proc->base_table, string__hash(range->name)
+ );
+ if (base > 0)
+ frame = get_native_frame(range->name, pc - base);
+ }
+ #endif
+ }
+ if (!isvalid(frame)) {
+ if (unw_get_proc_name(&cursor, _native_buf, MAXLEN, &offset) == 0) {
+ scope = strdup(_native_buf);
+ }
+ else {
+ scope = strdup("");
+ offset = 0;
+ }
+ if (isvalid(range))
+ filename = strdup(range->name);
+ else {
+ sprintf(_native_buf, "native@%lx", pc);
+ filename = strdup(_native_buf);
+ }
+
+ frame = frame_new(filename, scope, offset);
+ if (!isvalid(frame)) {
+ log_ie("Failed to make native frame");
+ sfree(filename);
+ sfree(scope);
+ FAIL;
+ }
+ }
+
+ lru_cache__store(cache, (key_dt) pc, (value_t) frame);
}
- sprintf(_stack[_stackp].code.filename, "native@%lx", pc);
- _stackp++;
- } while (_stackp < MAX_STACK_SIZE && unw_step(&cursor) > 0);
+ stack_native_push(frame);
+ } while (!stack_native_full() && unw_step(&cursor) > 0);
SUCCESS;
}
@@ -636,10 +528,14 @@ _py_thread__unwind_native_frame_stack(py_thread_t * self) {
// ----------------------------------------------------------------------------
int
py_thread__fill_from_raddr(py_thread_t * self, raddr_t * raddr, py_proc_t * proc) {
+ if (!isvalid(self))
+ FAIL;
+
+ V_DESC(proc->py_v);
+
PyThreadState ts;
- self->invalid = 1;
- self->stack_height = 0;
+ self->invalid = TRUE;
if (fail(copy_from_raddr(raddr, ts))) {
log_ie("Cannot read remote PyThreadState");
@@ -648,20 +544,18 @@ py_thread__fill_from_raddr(py_thread_t * self, raddr_t * raddr, py_proc_t * proc
self->proc = proc;
- self->raddr.pid = raddr->pid;
- self->raddr.addr = raddr->addr;
-
+ self->raddr = *raddr;
+
+ self->top_frame = V_FIELD(void*, ts, py_thread, o_frame);
- if (isvalid(self->top_frame = V_FIELD(void*, ts, py_thread, o_frame))) {
- self->stack_height = 1;
- }
-
- self->next_raddr.pid = raddr->pid;
- self->next_raddr.addr = V_FIELD(void*, ts, py_thread, o_next) == raddr->addr \
- ? NULL \
- : V_FIELD(void*, ts, py_thread, o_next);
+ self->next_raddr = (raddr_t) {
+ raddr->pid,
+ V_FIELD(void*, ts, py_thread, o_next) == raddr->addr \
+ ? NULL \
+ : V_FIELD(void*, ts, py_thread, o_next)
+ };
- self->tid = V_FIELD(long, ts, py_thread, o_thread_id);
+ self->tid = V_FIELD(long, ts, py_thread, o_thread_id);
if (self->tid == 0) {
// If we fail to get a valid Thread ID, we resort to the PyThreadState
// remote address
@@ -670,26 +564,20 @@ py_thread__fill_from_raddr(py_thread_t * self, raddr_t * raddr, py_proc_t * proc
}
#if defined PL_LINUX
else {
- // Try to determine the TID by reading the remote struct pthread structure.
- // We can then use this information to parse the appropriate procfs file and
- // determine the native thread's running state.
- if (unlikely(_pthread_tid_offset == 0)) {
- _infer_tid_field_offset(self);
- if (unlikely(_pthread_tid_offset == 0)) {
- log_d("tid field offset not ready");
- }
- }
- if (likely(_pthread_tid_offset != 0) && success(copy_memory(
- self->raddr.pid,
- (void *) self->tid,
- PTHREAD_BUFFER_SIZE * sizeof(void *),
- _pthread_buffer
+ if (
+ likely(proc->extra->pthread_tid_offset)
+ && success(read_pthread_t(self->raddr.pid, (void *) self->tid
))) {
- self->tid = (uintptr_t) _pthread_buffer[_pthread_tid_offset];
+ int o = proc->extra->pthread_tid_offset;
+ self->tid = o > 0 ? _pthread_buffer[o] : (pid_t) ((pid_t *) _pthread_buffer)[-o];
+ if (self->tid >= max_pid || self->tid == 0) {
+ log_e("Invalid TID detected");
+ FAIL;
+ }
#ifdef NATIVE
// TODO: If a TID is reused we will never seize it!
if (!isvalid(_tids[self->tid])) {
- if (fail(ptrace(PTRACE_SEIZE, self->tid, 0, 0))) {
+ if (fail(wait_ptrace(PTRACE_SEIZE, self->tid, 0, 0))) {
log_e("ptrace: cannot seize thread %d: %d\n", self->tid, errno);
FAIL;
}
@@ -707,7 +595,7 @@ py_thread__fill_from_raddr(py_thread_t * self, raddr_t * raddr, py_proc_t * proc
}
#endif
- self->invalid = 0;
+ self->invalid = FALSE;
SUCCESS;
} /* py_thread__fill_from_raddr */
@@ -718,18 +606,14 @@ py_thread__next(py_thread_t * self) {
if (self->invalid || !isvalid(self->next_raddr.addr))
FAIL;
- raddr_t next_raddr = { .pid = self->next_raddr.pid, .addr = self->next_raddr.addr };
-
- return py_thread__fill_from_raddr(self, &next_raddr, self->proc);
+ return py_thread__fill_from_raddr(self, &(self->next_raddr), self->proc);
}
// ----------------------------------------------------------------------------
#if defined PL_WIN
- #define SAMPLE_HEAD "P%I64d;T%I64x"
#define MEM_METRIC "%I64d"
#else
- #define SAMPLE_HEAD "P%d;T%ld"
#define MEM_METRIC "%ld"
#endif
#define TIME_METRIC "%lu"
@@ -738,20 +622,13 @@ py_thread__next(py_thread_t * self) {
void
py_thread__print_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t mem_delta) {
- #if defined PL_LINUX
- // If we still don't have this offset then the thread ID is bonkers so we
- // do not emit the sample
- if (unlikely(_pthread_tid_offset == 0))
- return;
- #endif
-
if (!pargs.full && pargs.memory && mem_delta == 0)
return;
if (self->invalid)
return;
- if (self->stack_height == 0 && pargs.exclude_empty)
+ if (pargs.exclude_empty && stack_is_empty())
// Skip if thread has no frames and we want to exclude empty threads
return;
@@ -759,7 +636,7 @@ py_thread__print_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t
return;
int is_idle = FALSE;
- if (pargs.full || pargs.sleepless) {
+ if (pargs.full || pargs.sleepless || unlikely(pargs.where)) {
#ifdef NATIVE
size_t index = self->tid >> 3;
int offset = self->tid & 7;
@@ -771,16 +648,12 @@ py_thread__print_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t
if (!pargs.full && is_idle && pargs.sleepless) {
#ifdef NATIVE
// If we don't sample the threads stall :(
- _stackp = 0;
_py_thread__unwind_native_frame_stack(self);
#endif
return;
}
}
- // Reset the frame stack before unwinding
- _stackp = 0;
-
#ifdef NATIVE
// We sample the kernel frame stack BEFORE interrupting because otherwise
@@ -790,62 +663,84 @@ py_thread__print_collapsed_stack(py_thread_t * self, ctime_t time_delta, ssize_t
if (pargs.kernel) {
_py_thread__unwind_kernel_frame_stack(self);
}
- if (fail(_py_thread__unwind_native_frame_stack(self)))
+ if (fail(_py_thread__unwind_native_frame_stack(self))) {
+ log_ie("Failed to unwind native stack");
return;
+ }
- size_t basep = _stackp;
// Update the thread state to improve guarantees that it will be in sync with
// the native stack just collected
py_thread__fill_from_raddr(self, &self->raddr, self->proc);
#endif
// Group entries by thread.
- fprintf(pargs.output_file, SAMPLE_HEAD, self->proc->pid, self->tid);
-
- if (self->stack_height) {
+ fprintfp(
+ pargs.output_file, pargs.head_format, self->proc->pid, self->tid,
+ // These are relevant only in `where` mode
+ is_idle ? "💤" : "🚀",
+ self->proc->child ? "🧒" : ""
+ );
+
+ if (isvalid(self->top_frame)) {
if (fail(_py_thread__unwind_frame_stack(self))) {
fprintf(pargs.output_file, ";:INVALID:");
stats_count_error();
}
-
- #ifndef NATIVE
- // Append frames
- while (_stackp > 0) {
- py_code_t code = _stack[--_stackp].code;
- fprintf(pargs.output_file, pargs.format, code.filename, code.scope, code.lineno);
- }
- #endif
}
#ifdef NATIVE
- register int i = _stackp;
- register int j = basep;
-
- py_code_t * code;
- while (j-- > 0) {
- if (strstr(_stack[j].code.scope, "PyEval_EvalFrame")) {
- code = ((i <= basep) ? &(_stack[j].code) : &(_stack[--i].code));
+ while (!stack_native_is_empty()) {
+ frame_t * native_frame = stack_native_pop();
+ if (!isvalid(native_frame)) {
+ log_e("Invalid native frame");
+ break;
+ }
+ char * eval_frame_fn = strstr(native_frame->scope, "PyEval_EvalFrame");
+ int is_frame_eval = FALSE;
+ if (isvalid(eval_frame_fn)) {
+ char c = *(eval_frame_fn+16);
+ V_DESC(self->proc->py_v);
+ is_frame_eval = (c == 'D') || (PYVER_ATMOST(3, 5) && c == 'E');
+ }
+ if (!stack_is_empty() && is_frame_eval) {
+ // TODO: if the py stack is empty we have a mismatch.
+ frame_t * frame = stack_pop();
+ fprintf(pargs.output_file, pargs.format, frame->filename, frame->scope, frame->line);
}
else {
- code = &(_stack[j].code);
+ fprintf(pargs.output_file, pargs.native_format, native_frame->filename, native_frame->scope, native_frame->line);
}
- fprintf(pargs.output_file, pargs.format, code->filename, code->scope, code->lineno);
}
- if (i != basep) {
- log_e("Stack mismatch: left with %d Python frames after interleaving", i - basep);
+ if (!stack_is_empty()) {
+ log_d("Stack mismatch: left with %d Python frames after interleaving", stack_pointer());
austin_errno = ETHREADINV;
#ifdef DEBUG
- fprintf(pargs.output_file, ";:%ld FRAMES LEFT:", i - basep);
+ fprintf(pargs.output_file, ";:%ld FRAMES LEFT:", stack_pointer());
#endif
}
+ while (!stack_kernel_is_empty()) {
+ char * scope = stack_kernel_pop();
+ fprintf(pargs.output_file, pargs.kernel_format, scope);
+ free(scope);
+ }
+
+ #else
+ while (!stack_is_empty()) {
+ frame_t * frame = stack_pop();
+ fprintfp(pargs.output_file, pargs.format, frame->filename, frame->scope, frame->line);
+ }
#endif
+
if (pargs.gc && py_proc__is_gc_collecting(self->proc) == TRUE) {
fprintf(pargs.output_file, ";:GC:");
stats_gc_time(time_delta);
}
+ if (unlikely(pargs.where))
+ return;
+
// 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",
@@ -871,8 +766,7 @@ py_thread_allocate(void) {
if (isvalid(_stack))
SUCCESS;
- _stack = (py_frame_t *) calloc(MAX_STACK_SIZE, sizeof(py_frame_t));
- if (!isvalid(_stack))
+ if (fail(stack_allocate(MAX_STACK_SIZE)))
FAIL;
#if defined PL_WIN
@@ -885,21 +779,38 @@ py_thread_allocate(void) {
FAIL;
#endif
+ max_pid = pid_max() + 1;
+
#ifdef NATIVE
- size_t max = pid_max();
- _tids = (void **) calloc(max, sizeof(void *));
+ _tids = (void **) calloc(max_pid, sizeof(void *));
if (!isvalid(_tids))
- FAIL;
+ goto failed;
- _tids_idle = (unsigned char *) calloc(max >> 8, sizeof(unsigned char));
+ size_t bmsize = (max_pid >> 3) + 1;
+
+ _tids_idle = (unsigned char *) calloc(bmsize, sizeof(unsigned char));
if (!isvalid(_tids_idle))
- FAIL;
+ goto failed;
+
+ _tids_int = (unsigned char *) calloc(bmsize, sizeof(unsigned char));
+ if (!isvalid(_tids_int))
+ goto failed;
if (pargs.kernel) {
- _kstacks = (char **) calloc(max, sizeof(char *));
+ _kstacks = (char **) calloc(max_pid, sizeof(char *));
if (!isvalid(_kstacks))
- FAIL;
+ goto failed;
}
+ goto ok;
+
+failed:
+ sfree(_tids);
+ sfree(_tids_idle);
+ sfree(_tids_int);
+ sfree(_kstacks);
+ FAIL;
+
+ok:
#endif
SUCCESS;
@@ -913,17 +824,37 @@ py_thread_free(void) {
sfree(_pi_buffer);
#endif
- sfree(_stack);
+ log_d(
+ "Frame cache hit ratio: %d/%d (%0.2f%%)\n",
+ _frame_cache_total - _frame_cache_miss,
+ _frame_cache_total,
+ (_frame_cache_total - _frame_cache_miss) * 100.0 / _frame_cache_total
+ );
+
+ #ifdef DEBUG
+ if (_frames_total) {
+ log_d(
+ "Frame heaps hit ratio: %d/%d (%0.2f%%)\n",
+ _frames_total - _frames_miss,
+ _frames_total,
+ (_frames_total - _frames_miss) * 100.0 / _frames_total
+ );
+ }
+ #endif
+
+ stack_deallocate();
sfree(_frames.content);
sfree(_frames_heap.content);
#ifdef NATIVE
- pid_t max_pid = pid_max();
for (pid_t tid = 0; tid < max_pid; tid++) {
if (isvalid(_tids[tid])) {
_UPT_destroy(_tids[tid]);
- ptrace(PTRACE_DETACH, tid, 0, 0);
- log_d("ptrace: thread %ld detached", tid);
+ if (fail(wait_ptrace(PTRACE_DETACH, tid, 0, 0))) {
+ log_d("ptrace: failed to detach thread %ld", tid);
+ } else {
+ log_d("ptrace: thread %ld detached", tid);
+ }
}
if (isvalid(_kstacks) && isvalid(_kstacks[tid])) {
sfree(_kstacks[tid]);
@@ -931,6 +862,7 @@ py_thread_free(void) {
}
sfree(_tids);
sfree(_tids_idle);
+ sfree(_tids_int);
sfree(_kstacks);
#endif
}
diff --git a/src/py_thread.h b/src/py_thread.h
index f02013ec..cb190d9a 100644
--- a/src/py_thread.h
+++ b/src/py_thread.h
@@ -31,6 +31,10 @@
#include "stats.h"
+#define MAXLEN 1024
+#define MAX_STACK_SIZE 2048
+
+
typedef struct thread {
raddr_t raddr;
raddr_t next_raddr;
@@ -40,7 +44,6 @@ typedef struct thread {
uintptr_t tid;
struct thread * next;
- size_t stack_height;
void * top_frame;
int invalid;
@@ -99,6 +102,12 @@ py_thread_free(void);
int
py_thread__set_idle(py_thread_t *);
+int
+py_thread__set_interrupted(py_thread_t *, int);
+
+int
+py_thread__is_interrupted(py_thread_t * self);
+
int
py_thread__save_kernel_stack(py_thread_t *);
#endif
diff --git a/src/python.h b/src/python.h
deleted file mode 100644
index c40ea3ea..00000000
--- a/src/python.h
+++ /dev/null
@@ -1,653 +0,0 @@
-// 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) 2018 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 .
-//
-// COPYRIGHT NOTICE: The content of this file is composed of different parts
-// taken from different versions of the source code of
-// Python. The authors of those sources hold the copyright
-// for most of the content of this header file.
-
-#ifndef PYTHON_H
-#define PYTHON_H
-
-#include
-#include
-
-
-// ---- object.h --------------------------------------------------------------
-
-#define PyObject_HEAD PyObject ob_base;
-#define PyObject_VAR_HEAD PyVarObject ob_base;
-
-#ifdef Py_TRACE_REFS
-#define _PyObject_HEAD_EXTRA \
- struct _object *_ob_next; \
- struct _object *_ob_prev;
-
-#define _PyObject_EXTRA_INIT 0, 0,
-
-#else
-#define _PyObject_HEAD_EXTRA
-#define _PyObject_EXTRA_INIT
-#endif
-
-
-typedef ssize_t Py_ssize_t;
-
-typedef struct _object {
- _PyObject_HEAD_EXTRA
- ssize_t ob_refcnt;
- struct _typeobject *ob_type;
-} PyObject;
-
-
-typedef struct {
- PyObject ob_base;
- Py_ssize_t ob_size; /* Number of items in variable part */
-} PyVarObject;
-
-
-// ---- code.h ----------------------------------------------------------------
-
-typedef struct {
- PyObject_HEAD
- int co_argcount; /* #arguments, except *args */
- int co_nlocals; /* #local variables */
- int co_stacksize; /* #entries needed for evaluation stack */
- int co_flags; /* CO_..., see below */
- PyObject *co_code; /* instruction opcodes */
- PyObject *co_consts; /* list (constants used) */
- PyObject *co_names; /* list of strings (names used) */
- PyObject *co_varnames; /* tuple of strings (local variable names) */
- PyObject *co_freevars; /* tuple of strings (free variable names) */
- PyObject *co_cellvars; /* tuple of strings (cell variable names) */
- PyObject *co_filename; /* string (where it was loaded from) */
- PyObject *co_name; /* string (name, for reference) */
- int co_firstlineno; /* first source line number */
- PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
-} PyCodeObject2;
-
-typedef struct {
- PyObject_HEAD
- int co_argcount; /* #arguments, except *args */
- int co_kwonlyargcount; /* #keyword only arguments */
- int co_nlocals; /* #local variables */
- int co_stacksize; /* #entries needed for evaluation stack */
- int co_flags; /* CO_..., see below */
- PyObject *co_code; /* instruction opcodes */
- PyObject *co_consts; /* list (constants used) */
- PyObject *co_names; /* list of strings (names used) */
- PyObject *co_varnames; /* tuple of strings (local variable names) */
- PyObject *co_freevars; /* tuple of strings (free variable names) */
- PyObject *co_cellvars; /* tuple of strings (cell variable names) */
- unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
- PyObject *co_filename; /* unicode (where it was loaded from) */
- PyObject *co_name; /* unicode (name, for reference) */
- int co_firstlineno; /* first source line number */
- PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
-} PyCodeObject3_3;
-
-typedef struct {
- PyObject_HEAD
- int co_argcount; /* #arguments, except *args */
- int co_kwonlyargcount; /* #keyword only arguments */
- int co_nlocals; /* #local variables */
- int co_stacksize; /* #entries needed for evaluation stack */
- int co_flags; /* CO_..., see below */
- int co_firstlineno; /* first source line number */
- PyObject *co_code; /* instruction opcodes */
- PyObject *co_consts; /* list (constants used) */
- PyObject *co_names; /* list of strings (names used) */
- PyObject *co_varnames; /* tuple of strings (local variable names) */
- PyObject *co_freevars; /* tuple of strings (free variable names) */
- PyObject *co_cellvars; /* tuple of strings (cell variable names) */
- unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
- PyObject *co_filename; /* unicode (where it was loaded from) */
- PyObject *co_name; /* unicode (name, for reference) */
- PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
-} PyCodeObject3_6;
-
-typedef struct {
- PyObject_HEAD
- int co_argcount; /* #arguments, except *args */
- int co_posonlyargcount; /* #positional only arguments */
- int co_kwonlyargcount; /* #keyword only arguments */
- int co_nlocals; /* #local variables */
- int co_stacksize; /* #entries needed for evaluation stack */
- int co_flags; /* CO_..., see below */
- int co_firstlineno; /* first source line number */
- PyObject *co_code; /* instruction opcodes */
- PyObject *co_consts; /* list (constants used) */
- PyObject *co_names; /* list of strings (names used) */
- PyObject *co_varnames; /* tuple of strings (local variable names) */
- PyObject *co_freevars; /* tuple of strings (free variable names) */
- PyObject *co_cellvars; /* tuple of strings (cell variable names) */
- Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */
- PyObject *co_filename; /* unicode (where it was loaded from) */
- PyObject *co_name; /* unicode (name, for reference) */
- PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
-} PyCodeObject3_8;
-
-typedef union {
- PyCodeObject2 v2;
- PyCodeObject3_3 v3_3;
- PyCodeObject3_6 v3_6;
- PyCodeObject3_8 v3_8;
-} PyCodeObject;
-
-
-// ---- frameobject.h ---------------------------------------------------------
-
-typedef struct _frame2_3 {
- PyObject_VAR_HEAD
- struct _frame2_3 *f_back; /* previous frame, or NULL */
- PyCodeObject *f_code; /* code segment */
- PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
- PyObject *f_globals; /* global symbol table (PyDictObject) */
- PyObject *f_locals; /* local symbol table (any mapping) */
- PyObject **f_valuestack; /* points after the last local */
- PyObject **f_stacktop;
- PyObject *f_trace; /* Trace function */
-
- PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
- PyObject *f_gen;
-
- int f_lasti; /* Last instruction if called */
- int f_lineno; /* Current line number */
-} PyFrameObject2;
-
-typedef struct _frame3_7 {
- PyObject_VAR_HEAD
- struct _frame3_7 *f_back; /* previous frame, or NULL */
- PyCodeObject *f_code; /* code segment */
- PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
- PyObject *f_globals; /* global symbol table (PyDictObject) */
- PyObject *f_locals; /* local symbol table (any mapping) */
- PyObject **f_valuestack; /* points after the last local */
- PyObject **f_stacktop;
- PyObject *f_trace; /* Trace function */
- char f_trace_lines; /* Emit per-line trace events? */
- char f_trace_opcodes; /* Emit per-opcode trace events? */
- PyObject *f_gen;
-
- int f_lasti; /* Last instruction if called */
- int f_lineno; /* Current line number */
-} PyFrameObject3_7;
-
-typedef struct _frame3_10 {
- PyObject_VAR_HEAD
- struct _frame3_10 *f_back; /* previous frame, or NULL */
- PyCodeObject *f_code; /* code segment */
- PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
- PyObject *f_globals; /* global symbol table (PyDictObject) */
- PyObject *f_locals; /* local symbol table (any mapping) */
- PyObject **f_valuestack; /* points after the last local */
- PyObject *f_trace; /* Trace function */
- int f_stackdepth; /* Depth of value stack */
- char f_trace_lines; /* Emit per-line trace events? */
- char f_trace_opcodes; /* Emit per-opcode trace events? */
-
- /* Borrowed reference to a generator, or NULL */
- PyObject *f_gen;
-
- int f_lasti; /* Last instruction if called */
- int f_lineno; /* Current line number. Only valid if non-zero */
-} PyFrameObject3_10;
-
-typedef union {
- PyFrameObject2 v2;
- PyFrameObject3_7 v3_7;
- 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 */
-
-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;
-
-typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
-
-
-typedef struct _ts3_3 {
- struct _ts3_3 *next;
- PyInterpreterState *interp;
-
- struct _frame *frame;
- int recursion_depth;
- char overflowed;
- char recursion_critical;
- int tracing;
- int use_tracing;
-
- Py_tracefunc c_profilefunc;
- Py_tracefunc c_tracefunc;
- PyObject *c_profileobj;
- PyObject *c_traceobj;
-
- PyObject *curexc_type;
- PyObject *curexc_value;
- PyObject *curexc_traceback;
-
- PyObject *exc_type;
- PyObject *exc_value;
- PyObject *exc_traceback;
-
- PyObject *dict; /* Stores per-thread state */
-
- int tick_counter;
-
- int gilstate_counter;
-
- PyObject *async_exc; /* Asynchronous exception to raise */
- long thread_id; /* Thread id where this tstate was created */
-} PyThreadState2;
-
-
-typedef struct _ts3_4 {
- struct _ts3_4 *prev;
- struct _ts3_4 *next;
- PyInterpreterState *interp;
-
- struct _frame *frame;
- int recursion_depth;
- char overflowed;
- char recursion_critical;
- int tracing;
- int use_tracing;
-
- Py_tracefunc c_profilefunc;
- Py_tracefunc c_tracefunc;
- PyObject *c_profileobj;
- PyObject *c_traceobj;
-
- PyObject *curexc_type;
- PyObject *curexc_value;
- PyObject *curexc_traceback;
-
- PyObject *exc_type;
- PyObject *exc_value;
- PyObject *exc_traceback;
-
- PyObject *dict; /* Stores per-thread state */
-
- int gilstate_counter;
-
- PyObject *async_exc; /* Asynchronous exception to raise */
- long thread_id; /* Thread id where this tstate was created */
-} PyThreadState3_4;
-
-
-
-typedef struct _err_stackitem {
- PyObject *exc_type, *exc_value, *exc_traceback;
- struct _err_stackitem *previous_item;
-} _PyErr_StackItem;
-
-typedef struct _ts_3_7 {
- struct _ts *prev;
- struct _ts *next;
- PyInterpreterState *interp;
- struct _frame *frame;
- int recursion_depth;
- char overflowed;
- char recursion_critical;
- int stackcheck_counter;
- int tracing;
- int use_tracing;
- Py_tracefunc c_profilefunc;
- Py_tracefunc c_tracefunc;
- PyObject *c_profileobj;
- PyObject *c_traceobj;
- PyObject *curexc_type;
- PyObject *curexc_value;
- PyObject *curexc_traceback;
- _PyErr_StackItem exc_state;
- _PyErr_StackItem *exc_info;
- PyObject *dict; /* Stores per-thread state */
- int gilstate_counter;
- PyObject *async_exc; /* Asynchronous exception to raise */
- unsigned long thread_id; /* Thread id where this tstate was created */
-} PyThreadState3_7;
-
-
-typedef struct _ts3_8 {
- struct _ts *prev;
- struct _ts *next;
- PyInterpreterState *interp;
- PyFrameObject *frame;
- int recursion_depth;
- int recursion_headroom; /* Allow 50 more calls to handle any errors. */
- int stackcheck_counter;
- int tracing;
- int use_tracing;
- Py_tracefunc c_profilefunc;
- Py_tracefunc c_tracefunc;
- PyObject *c_profileobj;
- PyObject *c_traceobj;
- PyObject *curexc_type;
- PyObject *curexc_value;
- PyObject *curexc_traceback;
- _PyErr_StackItem exc_state;
- _PyErr_StackItem *exc_info;
- PyObject *dict; /* Stores per-thread state */
- int gilstate_counter;
- PyObject *async_exc; /* Asynchronous exception to raise */
- unsigned long thread_id; /* Thread id where this tstate was created */
- int trash_delete_nesting;
- PyObject *trash_delete_later;
- void (*on_delete)(void *);
- void *on_delete_data;
- int coroutine_origin_tracking_depth;
- PyObject *async_gen_firstiter;
- PyObject *async_gen_finalizer;
- PyObject *context;
- uint64_t context_ver;
- uint64_t id;
-} PyThreadState3_8;
-
-
-typedef union {
- PyThreadState2 v2;
- PyThreadState3_4 v3_4;
- PyThreadState3_8 v3_8;
-} PyThreadState;
-
-// ---- internal/pystate.h ----------------------------------------------------
-
-typedef struct pyruntimestate3_7 {
- int initialized;
- int core_initialized;
- PyThreadState *finalizing;
-
- struct pyinterpreters3_7 {
- PyThread_type_lock mutex;
- PyInterpreterState *head;
- 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 ---------------------------------------------
-
-typedef struct pyruntimestate3_8 {
- int preinitializing;
- int preinitialized;
- int core_initialized;
- int initialized;
- PyThreadState *finalizing;
-
- struct pyinterpreters3_8 {
- PyThread_type_lock mutex;
- PyInterpreterState *head;
- 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;
-
-
-typedef union {
- _PyRuntimeState3_7 v3_7;
- _PyRuntimeState3_8 v3_8;
-} _PyRuntimeState;
-
-// ---- unicodeobject.h -------------------------------------------------------
-
-typedef uint32_t Py_UCS4;
-typedef uint16_t Py_UCS2;
-typedef uint8_t Py_UCS1;
-
-#define PY_UNICODE_TYPE Py_UCS4
-
-typedef PY_UNICODE_TYPE Py_UNICODE;
-
-
-typedef struct {
- PyObject_HEAD
- Py_ssize_t length; /* Length of raw Unicode data in buffer */
- Py_UNICODE *str; /* Raw Unicode buffer */
- long hash; /* Hash value; -1 if not set */
- PyObject *defenc; /* (Default) Encoded version as Python string */
-} PyUnicodeObject2;
-
-
-typedef Py_ssize_t Py_hash_t;
-
-typedef struct {
- PyObject_HEAD
- Py_ssize_t length; /* Number of code points in the string */
- Py_hash_t hash; /* Hash value; -1 if not set */
- struct {
- unsigned int interned:2;
- unsigned int kind:3;
- unsigned int compact:1;
- unsigned int ascii:1;
- unsigned int ready:1;
- unsigned int :24;
- } state;
- wchar_t *wstr; /* wchar_t representation (null-terminated) */
-} PyASCIIObject;
-
-typedef struct {
- PyASCIIObject _base;
- Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
- * terminating \0. */
- char *utf8; /* UTF-8 representation (null-terminated) */
- Py_ssize_t wstr_length; /* Number of code points in wstr, possible
- * surrogates count as two code points. */
-} PyCompactUnicodeObject;
-
-
-typedef struct {
- PyCompactUnicodeObject _base;
- union {
- void *any;
- Py_UCS1 *latin1;
- Py_UCS2 *ucs2;
- Py_UCS4 *ucs4;
- } data; /* Canonical, smallest-form Unicode buffer */
-} PyUnicodeObject3;
-
-
-typedef union {
- PyUnicodeObject2 v2;
- PyUnicodeObject3 v3;
-} PyUnicodeObject;
-
-// ---- bytesobject.h ---------------------------------------------------------
-
-typedef struct {
- PyObject_VAR_HEAD
- Py_hash_t ob_shash;
- char ob_sval[1];
-} PyBytesObject;
-
-
-// ---- stringobject.h --------------------------------------------------------
-
-typedef struct {
- PyObject_VAR_HEAD
- long ob_shash;
- int ob_sstate;
- char ob_sval[1];
-} PyStringObject; /* From Python 2.7 */
-
-
-// ----------------------------------------------------------------------------
-
-#endif // PYTHON36_H
diff --git a/src/dict.h b/src/python/abi.h
similarity index 76%
rename from src/dict.h
rename to src/python/abi.h
index 935c5f18..db47072c 100644
--- a/src/dict.h
+++ b/src/python/abi.h
@@ -5,7 +5,7 @@
//
// Austin is a Python frame stack sampler for CPython.
//
-// Copyright (c) 2018 Gabriele N. Tornetta .
+// Copyright (c) 2018-2022 Gabriele N. Tornetta .
// All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
@@ -20,5 +20,16 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-long
-string_hash(char *);
+#ifndef PYTHON_ABI_H
+#define PYTHON_ABI_H
+
+#include "code.h"
+#include "frame.h"
+#include "gc.h"
+#include "interp.h"
+#include "object.h"
+#include "runtime.h"
+#include "string.h"
+#include "thread.h"
+
+#endif
diff --git a/src/python/code.h b/src/python/code.h
new file mode 100644
index 00000000..287cc701
--- /dev/null
+++ b/src/python/code.h
@@ -0,0 +1,121 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_CODE_H
+#define PYTHON_CODE_H
+
+#include "object.h"
+
+// ---- code.h ----------------------------------------------------------------
+
+typedef struct {
+ PyObject_HEAD
+ int co_argcount; /* #arguments, except *args */
+ int co_nlocals; /* #local variables */
+ int co_stacksize; /* #entries needed for evaluation stack */
+ int co_flags; /* CO_..., see below */
+ PyObject *co_code; /* instruction opcodes */
+ PyObject *co_consts; /* list (constants used) */
+ PyObject *co_names; /* list of strings (names used) */
+ PyObject *co_varnames; /* tuple of strings (local variable names) */
+ PyObject *co_freevars; /* tuple of strings (free variable names) */
+ PyObject *co_cellvars; /* tuple of strings (cell variable names) */
+ PyObject *co_filename; /* string (where it was loaded from) */
+ PyObject *co_name; /* string (name, for reference) */
+ int co_firstlineno; /* first source line number */
+ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
+} PyCodeObject2;
+
+typedef struct {
+ PyObject_HEAD
+ int co_argcount; /* #arguments, except *args */
+ int co_kwonlyargcount; /* #keyword only arguments */
+ int co_nlocals; /* #local variables */
+ int co_stacksize; /* #entries needed for evaluation stack */
+ int co_flags; /* CO_..., see below */
+ PyObject *co_code; /* instruction opcodes */
+ PyObject *co_consts; /* list (constants used) */
+ PyObject *co_names; /* list of strings (names used) */
+ PyObject *co_varnames; /* tuple of strings (local variable names) */
+ PyObject *co_freevars; /* tuple of strings (free variable names) */
+ PyObject *co_cellvars; /* tuple of strings (cell variable names) */
+ unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
+ PyObject *co_filename; /* unicode (where it was loaded from) */
+ PyObject *co_name; /* unicode (name, for reference) */
+ int co_firstlineno; /* first source line number */
+ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
+} PyCodeObject3_3;
+
+typedef struct {
+ PyObject_HEAD
+ int co_argcount; /* #arguments, except *args */
+ int co_kwonlyargcount; /* #keyword only arguments */
+ int co_nlocals; /* #local variables */
+ int co_stacksize; /* #entries needed for evaluation stack */
+ int co_flags; /* CO_..., see below */
+ int co_firstlineno; /* first source line number */
+ PyObject *co_code; /* instruction opcodes */
+ PyObject *co_consts; /* list (constants used) */
+ PyObject *co_names; /* list of strings (names used) */
+ PyObject *co_varnames; /* tuple of strings (local variable names) */
+ PyObject *co_freevars; /* tuple of strings (free variable names) */
+ PyObject *co_cellvars; /* tuple of strings (cell variable names) */
+ unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
+ PyObject *co_filename; /* unicode (where it was loaded from) */
+ PyObject *co_name; /* unicode (name, for reference) */
+ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
+} PyCodeObject3_6;
+
+typedef struct {
+ PyObject_HEAD
+ int co_argcount; /* #arguments, except *args */
+ int co_posonlyargcount; /* #positional only arguments */
+ int co_kwonlyargcount; /* #keyword only arguments */
+ int co_nlocals; /* #local variables */
+ int co_stacksize; /* #entries needed for evaluation stack */
+ int co_flags; /* CO_..., see below */
+ int co_firstlineno; /* first source line number */
+ PyObject *co_code; /* instruction opcodes */
+ PyObject *co_consts; /* list (constants used) */
+ PyObject *co_names; /* list of strings (names used) */
+ PyObject *co_varnames; /* tuple of strings (local variable names) */
+ PyObject *co_freevars; /* tuple of strings (free variable names) */
+ PyObject *co_cellvars; /* tuple of strings (cell variable names) */
+ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */
+ PyObject *co_filename; /* unicode (where it was loaded from) */
+ PyObject *co_name; /* unicode (name, for reference) */
+ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) */
+} PyCodeObject3_8;
+
+typedef union {
+ PyCodeObject2 v2;
+ PyCodeObject3_3 v3_3;
+ PyCodeObject3_6 v3_6;
+ PyCodeObject3_8 v3_8;
+} PyCodeObject;
+
+#endif
\ No newline at end of file
diff --git a/src/python/frame.h b/src/python/frame.h
new file mode 100644
index 00000000..76a471e8
--- /dev/null
+++ b/src/python/frame.h
@@ -0,0 +1,98 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_FRAME_H
+#define PYTHON_FRAME_H
+
+#include "code.h"
+#include "object.h"
+
+// ---- frameobject.h ---------------------------------------------------------
+
+typedef struct _frame2_3 {
+ PyObject_VAR_HEAD
+ struct _frame2_3 *f_back; /* previous frame, or NULL */
+ PyCodeObject *f_code; /* code segment */
+ PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
+ PyObject *f_globals; /* global symbol table (PyDictObject) */
+ PyObject *f_locals; /* local symbol table (any mapping) */
+ PyObject **f_valuestack; /* points after the last local */
+ PyObject **f_stacktop;
+ PyObject *f_trace; /* Trace function */
+
+ PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
+ PyObject *f_gen;
+
+ int f_lasti; /* Last instruction if called */
+ int f_lineno; /* Current line number */
+} PyFrameObject2;
+
+typedef struct _frame3_7 {
+ PyObject_VAR_HEAD
+ struct _frame3_7 *f_back; /* previous frame, or NULL */
+ PyCodeObject *f_code; /* code segment */
+ PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
+ PyObject *f_globals; /* global symbol table (PyDictObject) */
+ PyObject *f_locals; /* local symbol table (any mapping) */
+ PyObject **f_valuestack; /* points after the last local */
+ PyObject **f_stacktop;
+ PyObject *f_trace; /* Trace function */
+ char f_trace_lines; /* Emit per-line trace events? */
+ char f_trace_opcodes; /* Emit per-opcode trace events? */
+ PyObject *f_gen;
+
+ int f_lasti; /* Last instruction if called */
+ int f_lineno; /* Current line number */
+} PyFrameObject3_7;
+
+typedef struct _frame3_10 {
+ PyObject_VAR_HEAD
+ struct _frame3_10 *f_back; /* previous frame, or NULL */
+ PyCodeObject *f_code; /* code segment */
+ PyObject *f_builtins; /* builtin symbol table (PyDictObject) */
+ PyObject *f_globals; /* global symbol table (PyDictObject) */
+ PyObject *f_locals; /* local symbol table (any mapping) */
+ PyObject **f_valuestack; /* points after the last local */
+ PyObject *f_trace; /* Trace function */
+ int f_stackdepth; /* Depth of value stack */
+ char f_trace_lines; /* Emit per-line trace events? */
+ char f_trace_opcodes; /* Emit per-opcode trace events? */
+
+ /* Borrowed reference to a generator, or NULL */
+ PyObject *f_gen;
+
+ int f_lasti; /* Last instruction if called */
+ int f_lineno; /* Current line number. Only valid if non-zero */
+} PyFrameObject3_10;
+
+typedef union {
+ PyFrameObject2 v2;
+ PyFrameObject3_7 v3_7;
+ PyFrameObject3_10 v3_10;
+} PyFrameObject;
+
+#endif
diff --git a/src/python/gc.h b/src/python/gc.h
new file mode 100644
index 00000000..85ac238c
--- /dev/null
+++ b/src/python/gc.h
@@ -0,0 +1,106 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_GC_H
+#define PYTHON_GC_H
+
+#include
+
+#include "object.h"
+
+// ---- 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;
+
+#endif
\ No newline at end of file
diff --git a/src/python/interp.h b/src/python/interp.h
new file mode 100644
index 00000000..22300b00
--- /dev/null
+++ b/src/python/interp.h
@@ -0,0 +1,100 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_INTERP_H
+#define PYTHON_INTERP_H
+
+#include
+
+#include "gc.h"
+
+// ---- pystate.h -------------------------------------------------------------
+
+struct _ts; /* Forward */
+
+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;
+
+#endif
\ No newline at end of file
diff --git a/src/python/object.h b/src/python/object.h
new file mode 100644
index 00000000..7b5b08fe
--- /dev/null
+++ b/src/python/object.h
@@ -0,0 +1,63 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_OBJECT_H
+#define PYTHON_OBJECT_H
+
+// ---- object.h --------------------------------------------------------------
+
+#define PyObject_HEAD PyObject ob_base;
+#define PyObject_VAR_HEAD PyVarObject ob_base;
+
+#ifdef Py_TRACE_REFS
+#define _PyObject_HEAD_EXTRA \
+ struct _object *_ob_next; \
+ struct _object *_ob_prev;
+
+#define _PyObject_EXTRA_INIT 0, 0,
+
+#else
+#define _PyObject_HEAD_EXTRA
+#define _PyObject_EXTRA_INIT
+#endif
+
+
+typedef ssize_t Py_ssize_t;
+
+typedef struct _object {
+ _PyObject_HEAD_EXTRA
+ ssize_t ob_refcnt;
+ struct _typeobject *ob_type;
+} PyObject;
+
+
+typedef struct {
+ PyObject ob_base;
+ Py_ssize_t ob_size; /* Number of items in variable part */
+} PyVarObject;
+
+#endif
\ No newline at end of file
diff --git a/src/python/runtime.h b/src/python/runtime.h
new file mode 100644
index 00000000..b4e8a627
--- /dev/null
+++ b/src/python/runtime.h
@@ -0,0 +1,95 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_RUNTIME_H
+#define PYTHON_RUNTIME_H
+
+#include "interp.h"
+#include "thread.h"
+
+// ---- internal/pystate.h ----------------------------------------------------
+
+typedef struct pyruntimestate3_7 {
+ int initialized;
+ int core_initialized;
+ PyThreadState *finalizing;
+
+ struct pyinterpreters3_7 {
+ PyThread_type_lock mutex;
+ PyInterpreterState *head;
+ 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 ---------------------------------------------
+
+typedef struct pyruntimestate3_8 {
+ int preinitializing;
+ int preinitialized;
+ int core_initialized;
+ int initialized;
+ PyThreadState *finalizing;
+
+ struct pyinterpreters3_8 {
+ PyThread_type_lock mutex;
+ PyInterpreterState *head;
+ 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;
+
+
+typedef union {
+ _PyRuntimeState3_7 v3_7;
+ _PyRuntimeState3_8 v3_8;
+} _PyRuntimeState;
+
+#endif
diff --git a/src/python/string.h b/src/python/string.h
new file mode 100644
index 00000000..56bd0a18
--- /dev/null
+++ b/src/python/string.h
@@ -0,0 +1,116 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_STRING_H
+#define PYTHON_STRING_H
+
+#include
+
+#include "object.h"
+
+// ---- unicodeobject.h -------------------------------------------------------
+
+typedef uint32_t Py_UCS4;
+typedef uint16_t Py_UCS2;
+typedef uint8_t Py_UCS1;
+
+#define PY_UNICODE_TYPE Py_UCS4
+
+typedef PY_UNICODE_TYPE Py_UNICODE;
+
+
+typedef struct {
+ PyObject_HEAD
+ Py_ssize_t length; /* Length of raw Unicode data in buffer */
+ Py_UNICODE *str; /* Raw Unicode buffer */
+ long hash; /* Hash value; -1 if not set */
+ PyObject *defenc; /* (Default) Encoded version as Python string */
+} PyUnicodeObject2;
+
+
+typedef Py_ssize_t Py_hash_t;
+
+typedef struct {
+ PyObject_HEAD
+ Py_ssize_t length; /* Number of code points in the string */
+ Py_hash_t hash; /* Hash value; -1 if not set */
+ struct {
+ unsigned int interned:2;
+ unsigned int kind:3;
+ unsigned int compact:1;
+ unsigned int ascii:1;
+ unsigned int ready:1;
+ unsigned int :24;
+ } state;
+ wchar_t *wstr; /* wchar_t representation (null-terminated) */
+} PyASCIIObject;
+
+typedef struct {
+ PyASCIIObject _base;
+ Py_ssize_t utf8_length; /* Number of bytes in utf8, excluding the
+ * terminating \0. */
+ char *utf8; /* UTF-8 representation (null-terminated) */
+ Py_ssize_t wstr_length; /* Number of code points in wstr, possible
+ * surrogates count as two code points. */
+} PyCompactUnicodeObject;
+
+
+typedef struct {
+ PyCompactUnicodeObject _base;
+ union {
+ void *any;
+ Py_UCS1 *latin1;
+ Py_UCS2 *ucs2;
+ Py_UCS4 *ucs4;
+ } data; /* Canonical, smallest-form Unicode buffer */
+} PyUnicodeObject3;
+
+
+typedef union {
+ PyUnicodeObject2 v2;
+ PyUnicodeObject3 v3;
+} PyUnicodeObject;
+
+// ---- bytesobject.h ---------------------------------------------------------
+
+typedef struct {
+ PyObject_VAR_HEAD
+ Py_hash_t ob_shash;
+ char ob_sval[1];
+} PyBytesObject;
+
+
+// ---- stringobject.h --------------------------------------------------------
+
+typedef struct {
+ PyObject_VAR_HEAD
+ long ob_shash;
+ int ob_sstate;
+ char ob_sval[1];
+} PyStringObject; /* From Python 2.7 */
+
+#endif
diff --git a/src/python/thread.h b/src/python/thread.h
new file mode 100644
index 00000000..e57e83f5
--- /dev/null
+++ b/src/python/thread.h
@@ -0,0 +1,186 @@
+// 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) 2018-2022 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 .
+//
+// COPYRIGHT NOTICE: The content of this file is composed of different parts
+// taken from different versions of the source code of
+// Python. The authors of those sources hold the copyright
+// for most of the content of this header file.
+
+#ifndef PYTHON_THREAD_H
+#define PYTHON_THREAD_H
+
+#include
+
+#include "frame.h"
+#include "object.h"
+
+// Dummy struct _frame
+struct _frame;
+
+typedef int (*Py_tracefunc)(PyObject *, struct _frame *, int, PyObject *);
+
+
+typedef struct _ts2 {
+ struct _ts2 *next;
+ PyInterpreterState *interp;
+
+ struct _frame *frame;
+ int recursion_depth;
+ char overflowed;
+ char recursion_critical;
+ int tracing;
+ int use_tracing;
+
+ Py_tracefunc c_profilefunc;
+ Py_tracefunc c_tracefunc;
+ PyObject *c_profileobj;
+ PyObject *c_traceobj;
+
+ PyObject *curexc_type;
+ PyObject *curexc_value;
+ PyObject *curexc_traceback;
+
+ PyObject *exc_type;
+ PyObject *exc_value;
+ PyObject *exc_traceback;
+
+ PyObject *dict; /* Stores per-thread state */
+
+ int tick_counter;
+
+ int gilstate_counter;
+
+ PyObject *async_exc; /* Asynchronous exception to raise */
+ long thread_id; /* Thread id where this tstate was created */
+} PyThreadState2;
+
+
+typedef struct _ts3_4 {
+ struct _ts3_4 *prev;
+ struct _ts3_4 *next;
+ PyInterpreterState *interp;
+
+ struct _frame *frame;
+ int recursion_depth;
+ char overflowed;
+ char recursion_critical;
+ int tracing;
+ int use_tracing;
+
+ Py_tracefunc c_profilefunc;
+ Py_tracefunc c_tracefunc;
+ PyObject *c_profileobj;
+ PyObject *c_traceobj;
+
+ PyObject *curexc_type;
+ PyObject *curexc_value;
+ PyObject *curexc_traceback;
+
+ PyObject *exc_type;
+ PyObject *exc_value;
+ PyObject *exc_traceback;
+
+ PyObject *dict; /* Stores per-thread state */
+
+ int gilstate_counter;
+
+ PyObject *async_exc; /* Asynchronous exception to raise */
+ long thread_id; /* Thread id where this tstate was created */
+} PyThreadState3_4;
+
+
+
+typedef struct _err_stackitem {
+ PyObject *exc_type, *exc_value, *exc_traceback;
+ struct _err_stackitem *previous_item;
+} _PyErr_StackItem;
+
+typedef struct _ts_3_7 {
+ struct _ts *prev;
+ struct _ts *next;
+ PyInterpreterState *interp;
+ struct _frame *frame;
+ int recursion_depth;
+ char overflowed;
+ char recursion_critical;
+ int stackcheck_counter;
+ int tracing;
+ int use_tracing;
+ Py_tracefunc c_profilefunc;
+ Py_tracefunc c_tracefunc;
+ PyObject *c_profileobj;
+ PyObject *c_traceobj;
+ PyObject *curexc_type;
+ PyObject *curexc_value;
+ PyObject *curexc_traceback;
+ _PyErr_StackItem exc_state;
+ _PyErr_StackItem *exc_info;
+ PyObject *dict; /* Stores per-thread state */
+ int gilstate_counter;
+ PyObject *async_exc; /* Asynchronous exception to raise */
+ unsigned long thread_id; /* Thread id where this tstate was created */
+} PyThreadState3_7;
+
+
+typedef struct _ts3_8 {
+ struct _ts *prev;
+ struct _ts *next;
+ PyInterpreterState *interp;
+ PyFrameObject *frame;
+ int recursion_depth;
+ int recursion_headroom; /* Allow 50 more calls to handle any errors. */
+ int stackcheck_counter;
+ int tracing;
+ int use_tracing;
+ Py_tracefunc c_profilefunc;
+ Py_tracefunc c_tracefunc;
+ PyObject *c_profileobj;
+ PyObject *c_traceobj;
+ PyObject *curexc_type;
+ PyObject *curexc_value;
+ PyObject *curexc_traceback;
+ _PyErr_StackItem exc_state;
+ _PyErr_StackItem *exc_info;
+ PyObject *dict; /* Stores per-thread state */
+ int gilstate_counter;
+ PyObject *async_exc; /* Asynchronous exception to raise */
+ unsigned long thread_id; /* Thread id where this tstate was created */
+ int trash_delete_nesting;
+ PyObject *trash_delete_later;
+ void (*on_delete)(void *);
+ void *on_delete_data;
+ int coroutine_origin_tracking_depth;
+ PyObject *async_gen_firstiter;
+ PyObject *async_gen_finalizer;
+ PyObject *context;
+ uint64_t context_ver;
+ uint64_t id;
+} PyThreadState3_8;
+
+
+typedef union {
+ PyThreadState2 v2;
+ PyThreadState3_4 v3_4;
+ PyThreadState3_8 v3_8;
+} PyThreadState;
+
+#endif
diff --git a/src/stack.h b/src/stack.h
new file mode 100644
index 00000000..6c84cd59
--- /dev/null
+++ b/src/stack.h
@@ -0,0 +1,280 @@
+// 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) 2018-2021 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 .
+
+#ifndef STACK_H
+#define STACK_H
+
+#include
+#include
+
+#include "hints.h"
+#include "py_string.h"
+#include "version.h"
+
+typedef struct {
+ char * filename;
+ char * scope;
+ unsigned int line;
+} frame_t;
+
+#ifdef PY_THREAD_C
+
+typedef struct {
+ void * origin;
+ void * code;
+ int lasti;
+} py_frame_t;
+
+typedef struct {
+ size_t size;
+ frame_t ** base;
+ ssize_t pointer;
+ py_frame_t * py_base;
+ #ifdef NATIVE
+ frame_t ** native_base;
+ ssize_t native_pointer;
+
+ char ** kernel_base;
+ ssize_t kernel_pointer;
+ #endif
+} stack_dt;
+
+static stack_dt * _stack;
+
+static inline frame_t *
+frame_new(char * filename, char * scope, unsigned int line) {
+ frame_t * frame = (frame_t *) malloc(sizeof(frame_t));
+ if (!isvalid(frame)) {
+ return NULL;
+ }
+
+ frame->filename = filename;
+ frame->scope = scope;
+ frame->line = line;
+
+ return frame;
+}
+
+static inline int
+stack_allocate(size_t size) {
+ if (isvalid(_stack))
+ SUCCESS;
+
+ _stack = (stack_dt *) calloc(1, sizeof(stack_dt));
+ if (!isvalid(_stack))
+ FAIL;
+
+ _stack->size = size;
+ _stack->base = (frame_t **) calloc(size, sizeof(frame_t *));
+ _stack->py_base = (py_frame_t *) calloc(size, sizeof(py_frame_t));
+ #ifdef NATIVE
+ _stack->native_base = (frame_t **) calloc(size, sizeof(frame_t *));
+ _stack->kernel_base = (char **) calloc(size, sizeof(char *));
+ #endif
+
+ SUCCESS;
+}
+
+static inline void
+stack_deallocate(void) {
+ if (!isvalid(_stack))
+ return;
+
+ free(_stack->base);
+ free(_stack->py_base);
+ #ifdef NATIVE
+ free(_stack->native_base);
+ free(_stack->kernel_base);
+ #endif
+
+ free(_stack);
+}
+
+static inline int
+stack_has_cycle(void) {
+ if (_stack->pointer < 2)
+ return FALSE;
+
+ // This sucks! :( Worst case is quadratic in the stack height, but if the
+ // sampled stacks are short on average, it might still be faster than the
+ // overhead introduced by looking up from a set-like data structure.
+ py_frame_t top = _stack->py_base[_stack->pointer-1];
+ for (ssize_t i = _stack->pointer - 2; i >= 0; i--) {
+ if (top.origin == _stack->py_base[i].origin)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static inline void
+stack_py_push(void * origin, void * code, int lasti) {
+ _stack->py_base[_stack->pointer++] = (py_frame_t) {
+ .origin = origin,
+ .code = code,
+ .lasti = lasti
+ };
+}
+
+#define stack_pointer() (_stack->pointer)
+#define stack_push(frame) {_stack->base[_stack->pointer++] = frame;}
+#define stack_set(i, frame) {_stack->base[i] = frame;}
+#define stack_pop() (_stack->base[--_stack->pointer])
+#define stack_py_pop() (_stack->py_base[--_stack->pointer])
+#define stack_py_get(i) (_stack->py_base[i])
+#define stack_top() (_stack->pointer ? _stack->base[_stack->pointer-1] : NULL)
+#define stack_reset() {_stack->pointer = 0;}
+#define stack_is_valid() (_stack->base[_stack->pointer-1]->line != 0)
+#define stack_is_empty() (_stack->pointer == 0)
+#define stack_full() (_stack->pointer >= _stack->size)
+
+#ifdef NATIVE
+#define stack_native_push(frame) {_stack->native_base[_stack->native_pointer++] = frame;}
+#define stack_native_pop() (_stack->native_base[--_stack->native_pointer])
+#define stack_native_is_empty() (_stack->native_pointer == 0)
+#define stack_native_full() (_stack->native_pointer >= _stack->size)
+#define stack_native_reset() {_stack->native_pointer = 0;}
+
+#define stack_kernel_push(frame) {_stack->kernel_base[_stack->kernel_pointer++] = frame;}
+#define stack_kernel_pop() (_stack->kernel_base[--_stack->kernel_pointer])
+#define stack_kernel_is_empty() (_stack->kernel_pointer == 0)
+#define stack_kernel_full() (_stack->kernel_pointer >= _stack->size)
+#define stack_kernel_reset() {_stack->kernel_pointer = 0;}
+#endif
+
+
+// ----------------------------------------------------------------------------
+#define _code__get_filename(self, pid, py_v) \
+ _string_from_raddr( \
+ pid, *((void **) ((void *) self + py_v->py_code.o_filename)), py_v \
+ )
+
+#define _code__get_name(self, pid, py_v) \
+ _string_from_raddr( \
+ pid, *((void **) ((void *) self + py_v->py_code.o_name)), py_v \
+ )
+
+#define _code__get_lnotab(self, pid, len, py_v) \
+ _bytes_from_raddr( \
+ pid, *((void **) ((void *) self + py_v->py_code.o_lnotab)), len, py_v \
+ )
+
+
+// ----------------------------------------------------------------------------
+static inline frame_t *
+_frame_from_code_raddr(raddr_t * raddr, int lasti, python_v * py_v) {
+ PyCodeObject code;
+ unsigned char * lnotab = NULL;
+
+ if (fail(copy_from_raddr_v(raddr, code, py_v->py_code.size))) {
+ log_ie("Cannot read remote PyCodeObject");
+ return NULL;
+ }
+
+ char * filename = _code__get_filename(&code, raddr->pid, py_v);
+ if (!isvalid(filename)) {
+ log_ie("Cannot get file name from PyCodeObject");
+ return NULL;
+ }
+
+ char * scope = _code__get_name(&code, raddr->pid, py_v);
+ if (!isvalid(scope)) {
+ log_ie("Cannot get scope name from PyCodeObject");
+ goto failed;
+ }
+
+ ssize_t len = 0;
+ lnotab = _code__get_lnotab(&code, raddr->pid, &len, py_v);
+ if (!isvalid(lnotab) || len % 2) {
+ log_ie("Cannot get line number from PyCodeObject");
+ goto failed;
+ }
+
+ int lineno = V_FIELD(unsigned int, code, py_code, o_firstlineno);
+
+ if (py_v->major == 3 && py_v->minor >= 10) { // Python >=3.10
+ lasti <<= 1;
+ for (register int i = 0, bc = 0; i < len; i++) {
+ int sdelta = lnotab[i++];
+ if (sdelta == 0xff)
+ break;
+
+ bc += sdelta;
+
+ int ldelta = lnotab[i];
+ if (ldelta == 0x80)
+ ldelta = 0;
+ else if (ldelta > 0x80)
+ lineno -= 0x100;
+
+ lineno += ldelta;
+ if (bc > lasti)
+ break;
+ }
+ }
+ else { // Python < 3.10
+ for (register int i = 0, bc = 0; i < len; i++) {
+ bc += lnotab[i++];
+ if (bc > lasti)
+ break;
+
+ if (lnotab[i] >= 0x80)
+ lineno -= 0x100;
+
+ lineno += lnotab[i];
+ }
+ }
+
+ free(lnotab);
+
+ frame_t * frame = frame_new(filename, scope, lineno);
+ if (!isvalid(frame)) {
+ log_e("Failed to create frame object");
+ goto failed;
+ }
+
+ return frame;
+
+failed:
+ sfree(lnotab);
+ sfree(filename);
+ sfree(scope);
+
+ return NULL;
+}
+
+
+#endif // PY_THREAD_C
+
+
+// ----------------------------------------------------------------------------
+static inline void
+frame__destroy(frame_t * self) {
+ if (!isvalid(self))
+ return;
+
+ sfree(self->filename);
+ sfree(self->scope);
+
+ free(self);
+}
+
+#endif // STACK_H
diff --git a/src/version.c b/src/version.c
deleted file mode 100644
index 8bd7b869..00000000
--- a/src/version.c
+++ /dev/null
@@ -1,243 +0,0 @@
-// 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) 2018 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 .
-
-#define VERSION_C
-
-#include "logging.h"
-#include "platform.h"
-#include "version.h"
-
-
-#define UNSUPPORTED_VERSION log_w("Unsupported Python version detected. Austin might not work as expected.")
-
-#define LATEST_VERSION (&python_v3_10)
-
-#define PY_CODE(s) { \
- sizeof(s), \
- offsetof(s, co_filename), \
- offsetof(s, co_name), \
- offsetof(s, co_lnotab), \
- offsetof(s, co_firstlineno) \
-}
-
-#define PY_FRAME(s) { \
- sizeof(s), \
- offsetof(s, f_back), \
- offsetof(s, f_code), \
- offsetof(s, f_lasti), \
- offsetof(s, f_lineno), \
-}
-
-/* Hack. Python 3.3 and below don't have the prev field */
-#define PY_THREAD_H(s) { \
- sizeof(s), \
- offsetof(s, next), \
- offsetof(s, next), \
- offsetof(s, interp), \
- offsetof(s, frame), \
- offsetof(s, thread_id) \
-}
-
-#define PY_THREAD(s) { \
- sizeof(s), \
- offsetof(s, prev), \
- offsetof(s, next), \
- offsetof(s, interp), \
- offsetof(s, frame), \
- offsetof(s, thread_id) \
-}
-
-#define PY_UNICODE(n) { \
- n \
-}
-
-#define PY_BYTES(n) { \
- n \
-}
-
-#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 --------------------------------------------------------------
-
-python_v python_v2 = {
- PY_CODE (PyCodeObject2),
- PY_FRAME (PyFrameObject2),
- PY_THREAD_H (PyThreadState2),
- PY_IS (PyInterpreterState2),
-};
-
-// ---- Python 3.3 ------------------------------------------------------------
-
-python_v python_v3_3 = {
- PY_CODE (PyCodeObject3_3),
- PY_FRAME (PyFrameObject2),
- PY_THREAD_H (PyThreadState2),
- PY_IS (PyInterpreterState2),
-};
-
-// ---- Python 3.4 ------------------------------------------------------------
-
-python_v python_v3_4 = {
- PY_CODE (PyCodeObject3_3),
- PY_FRAME (PyFrameObject2),
- PY_THREAD (PyThreadState3_4),
- PY_IS (PyInterpreterState2),
-};
-
-// ---- Python 3.6 ------------------------------------------------------------
-
-python_v python_v3_6 = {
- PY_CODE (PyCodeObject3_6),
- PY_FRAME (PyFrameObject2),
- PY_THREAD (PyThreadState3_4),
- PY_IS (PyInterpreterState2),
-};
-
-// ---- Python 3.7 ------------------------------------------------------------
-
-python_v python_v3_7 = {
- PY_CODE (PyCodeObject3_6),
- PY_FRAME (PyFrameObject3_7),
- PY_THREAD (PyThreadState3_7),
- PY_IS (PyInterpreterState2),
- PY_RUNTIME (_PyRuntimeState3_7),
- PY_GC (struct _gc_runtime_state3_7),
-};
-
-// ---- Python 3.8 ------------------------------------------------------------
-
-python_v python_v3_8 = {
- PY_CODE (PyCodeObject3_8),
- PY_FRAME (PyFrameObject3_7),
- PY_THREAD (PyThreadState3_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_IS (PyInterpreterState3_9),
- PY_RUNTIME (_PyRuntimeState3_8),
- PY_GC (struct _gc_runtime_state3_8),
-};
-
-// ----------------------------------------------------------------------------
-void
-set_version(int version) {
- int minor = (version >> 8) & 0xFF;
- int major = (version >> 16) & 0xFF;
-
- switch (major) {
-
- // ---- Python 2 ------------------------------------------------------------
- case 2:
- switch (minor) {
- case 0:
- case 1:
- case 2:
- UNSUPPORTED_VERSION; // NOTE: These versions haven't been tested.
-
- // 2.3, 2.4, 2.5, 2.6, 2.7
- case 3:
- case 4:
- case 5:
- case 6:
- case 7: py_v = &python_v2;
- break;
-
- default: py_v = &python_v2;
- UNSUPPORTED_VERSION;
- }
- break;
-
- // ---- Python 3 ------------------------------------------------------------
- case 3:
- switch (minor) {
- case 0:
- case 1:
- case 2:
- UNSUPPORTED_VERSION; // NOTE: These versions haven't been tested.
-
- // 3.3
- case 3: py_v = &python_v3_3; break;
-
- // 3.4, 3.5
- case 4:
- case 5: py_v = &python_v3_4; break;
-
- // 3.6
- case 6: py_v = &python_v3_6; break;
-
- // 3.7
- case 7: py_v = &python_v3_7; 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;
-
- default: py_v = LATEST_VERSION;
- UNSUPPORTED_VERSION;
- }
- }
-
- py_v->major = major;
- py_v->minor = minor;
-}
diff --git a/src/version.h b/src/version.h
index 7ee7318b..411dd274 100644
--- a/src/version.h
+++ b/src/version.h
@@ -34,7 +34,9 @@
#include
#include
-#include "python.h"
+#include "logging.h"
+#include "platform.h"
+#include "python/abi.h"
#define PYVERSION(major, minor, patch) ((major << 16) | (minor << 8) | patch)
@@ -42,12 +44,18 @@
#define MINOR(x) ((x >> 8) & 0xFF)
#define PATCH(x) (x & 0xFF)
+#define PYVER_ATMOST(maj, min) \
+ (py_v->major < maj || (py_v->major == maj && py_v->minor <= min))
+
/**
* Get the value of a field of a versioned structure.
*
* It works by retrieving the field offset from the offset table set at
- * runtime, depending on the detected version of Python.
+ * runtime, depending on the detected version of Python. The scope in which
+ * these macros are used must have a local variable py_v of type python_v *
+ * declared (and initialised to a valid value). The V_DESC macro can be used to
+ * ensure this.
*
* @param ctype the C type of the field to retrieve, e.g. void *.
* @param py_obj the address of the beginning of the actual Python structure.
@@ -57,8 +65,13 @@
* @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_PTR(ctype, py_obj_ptr, py_type, field) (*((ctype*) (((void *) py_obj_ptr) + py_v->py_type.field)))
+#define V_FIELD(ctype, py_obj, py_type, field) \
+ (*((ctype*) (((void *) &py_obj) + py_v->py_type.field)))
+
+#define V_FIELD_PTR(ctype, py_obj_ptr, py_type, field) \
+ (*((ctype*) (((void *) py_obj_ptr) + py_v->py_type.field)))
+
+#define V_DESC(desc) python_v * py_v = (desc)
typedef unsigned long offset_t;
@@ -138,18 +151,233 @@ typedef struct {
int major;
int minor;
+ int patch;
} python_v;
-void
-set_version(int);
-
-
-#ifndef VERSION_C
-extern python_v * py_v;
-#else
-python_v * py_v;
-#endif
-
+#ifdef PY_PROC_C
+
+#define UNSUPPORTED_VERSION \
+ log_w("Unsupported Python version detected. Austin might not work as expected.")
+
+#define LATEST_VERSION (&python_v3_10)
+
+#define PY_CODE(s) { \
+ sizeof(s), \
+ offsetof(s, co_filename), \
+ offsetof(s, co_name), \
+ offsetof(s, co_lnotab), \
+ offsetof(s, co_firstlineno) \
+}
+
+#define PY_FRAME(s) { \
+ sizeof(s), \
+ offsetof(s, f_back), \
+ offsetof(s, f_code), \
+ offsetof(s, f_lasti), \
+ offsetof(s, f_lineno), \
+}
+
+/* Hack. Python 3.3 and below don't have the prev field */
+#define PY_THREAD_2(s) { \
+ sizeof(s), \
+ offsetof(s, next), \
+ offsetof(s, next), \
+ offsetof(s, interp), \
+ offsetof(s, frame), \
+ offsetof(s, thread_id) \
+}
+
+#define PY_THREAD(s) { \
+ sizeof(s), \
+ offsetof(s, prev), \
+ offsetof(s, next), \
+ offsetof(s, interp), \
+ offsetof(s, frame), \
+ offsetof(s, thread_id) \
+}
+
+#define PY_UNICODE(n) { \
+ n \
+}
+
+#define PY_BYTES(n) { \
+ n \
+}
+
+#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 --------------------------------------------------------------
+
+python_v python_v2 = {
+ PY_CODE (PyCodeObject2),
+ PY_FRAME (PyFrameObject2),
+ PY_THREAD_2 (PyThreadState2),
+ PY_IS (PyInterpreterState2),
+};
+
+// ---- Python 3.3 ------------------------------------------------------------
+
+python_v python_v3_3 = {
+ PY_CODE (PyCodeObject3_3),
+ PY_FRAME (PyFrameObject2),
+ PY_THREAD_2 (PyThreadState2),
+ PY_IS (PyInterpreterState2),
+};
+
+// ---- Python 3.4 ------------------------------------------------------------
+
+python_v python_v3_4 = {
+ PY_CODE (PyCodeObject3_3),
+ PY_FRAME (PyFrameObject2),
+ PY_THREAD (PyThreadState3_4),
+ PY_IS (PyInterpreterState2),
+};
+
+// ---- Python 3.6 ------------------------------------------------------------
+
+python_v python_v3_6 = {
+ PY_CODE (PyCodeObject3_6),
+ PY_FRAME (PyFrameObject2),
+ PY_THREAD (PyThreadState3_4),
+ PY_IS (PyInterpreterState2),
+};
+
+// ---- Python 3.7 ------------------------------------------------------------
+
+python_v python_v3_7 = {
+ PY_CODE (PyCodeObject3_6),
+ PY_FRAME (PyFrameObject3_7),
+ PY_THREAD (PyThreadState3_7),
+ PY_IS (PyInterpreterState2),
+ PY_RUNTIME (_PyRuntimeState3_7),
+ PY_GC (struct _gc_runtime_state3_7),
+};
+
+// ---- Python 3.8 ------------------------------------------------------------
+
+python_v python_v3_8 = {
+ PY_CODE (PyCodeObject3_8),
+ PY_FRAME (PyFrameObject3_7),
+ PY_THREAD (PyThreadState3_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_IS (PyInterpreterState3_9),
+ PY_RUNTIME (_PyRuntimeState3_8),
+ PY_GC (struct _gc_runtime_state3_8),
+};
+
+// ----------------------------------------------------------------------------
+static inline python_v *
+get_version_descriptor(int major, int minor, int patch) {
+ if (major == 0 && minor == 0)
+ return NULL;
+
+ python_v * py_v = NULL;
+
+ switch (major) {
+
+ // ---- Python 2 ------------------------------------------------------------
+ case 2:
+ switch (minor) {
+ case 0:
+ case 1:
+ case 2:
+ UNSUPPORTED_VERSION; // NOTE: These versions haven't been tested.
+
+ // 2.3, 2.4, 2.5, 2.6, 2.7
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7: py_v = &python_v2;
+ break;
+
+ default: py_v = &python_v2;
+ UNSUPPORTED_VERSION;
+ }
+ break;
+
+ // ---- Python 3 ------------------------------------------------------------
+ case 3:
+ switch (minor) {
+ case 0:
+ case 1:
+ case 2:
+ UNSUPPORTED_VERSION; // NOTE: These versions haven't been tested.
+
+ // 3.3
+ case 3: py_v = &python_v3_3; break;
+
+ // 3.4, 3.5
+ case 4:
+ case 5: py_v = &python_v3_4; break;
+
+ // 3.6
+ case 6: py_v = &python_v3_6; break;
+
+ // 3.7
+ case 7: py_v = &python_v3_7; 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;
+
+ default: py_v = LATEST_VERSION;
+ UNSUPPORTED_VERSION;
+ }
+ }
+
+ py_v->major = major;
+ py_v->minor = minor;
+ py_v->patch = patch;
+
+ return py_v;
+}
+
+#endif // PY_PROC_C
#endif
diff --git a/src/win/py_proc.h b/src/win/py_proc.h
index 8a8fc6ac..dc44e967 100644
--- a/src/win/py_proc.h
+++ b/src/win/py_proc.h
@@ -99,7 +99,7 @@ _py_proc__analyze_pe(py_proc_t * self, char * path) {
DWORD * addrs = (DWORD *) map_addr_from_rva(pMapping, e_dir->AddressOfFunctions);
for (
register int i = 0;
- self->sym_loaded < SYMBOLS && i < e_dir->NumberOfFunctions;
+ self->sym_loaded < SYMBOLS && i < e_dir->NumberOfNames;
i++
) {
char * sym_name = (char *) map_addr_from_rva(pMapping, names[i]);
@@ -130,6 +130,9 @@ _py_proc__get_modules(py_proc_t * self) {
self->min_raddr = (void *) -1;
self->max_raddr = NULL;
+ sfree(self->bin_path);
+ sfree(self->lib_path);
+
BOOL success = Module32First(mod_hdl, &module);
while (success) {
if ((void *) module.modBaseAddr < self->min_raddr)
@@ -143,7 +146,11 @@ _py_proc__get_modules(py_proc_t * self) {
module.modBaseAddr, module.modBaseAddr + module.modBaseSize,
module.szModule
);
- if (self->bin_path == NULL && strstr(module.szModule, ".exe")) {
+ if (
+ self->bin_path == NULL \
+ && strcmp(module.szModule, "py.exe") \
+ && strstr(module.szModule, ".exe") \
+ ) {
log_d("Candidate binary: %s (size %d KB)", module.szModule, module.modBaseSize >> 10);
self->bin_path = strdup(module.szExePath);
}
@@ -204,7 +211,7 @@ reader_thread(LPVOID lpParam) {
// ----------------------------------------------------------------------------
// Forward declaration.
static int
-_py_proc__run(py_proc_t *, int);
+_py_proc__run(py_proc_t *);
// On Windows, if we fail with the parent process we look if it has a single
@@ -255,12 +262,12 @@ with_resources;
log_e("Cannot open child process handle");
NOK;
}
- if (success(_py_proc__run(self, FALSE))) {
+ if (success(_py_proc__run(self))) {
log_d("Process has a single Python child with PID %d. We will attach to that", child_pid);
OK;
}
else {
- log_d("Process had a single non-Python child with PID %d. Taking it as new parent", child_pid);
+ log_d("Process has a single non-Python child with PID %d. Taking it as new parent", child_pid);
CloseHandle(self->extra->h_proc);
}
}
diff --git a/src/win/py_thread.h b/src/win/py_thread.h
index 5e80a931..fa0e0a6e 100644
--- a/src/win/py_thread.h
+++ b/src/win/py_thread.h
@@ -43,7 +43,12 @@ _py_thread__is_idle(py_thread_t * self) {
if (status == STATUS_INFO_LENGTH_MISMATCH) {
// Buffer was too small so we reallocate a larger one and try again.
_pi_buffer_size = n;
- _pi_buffer = realloc(_pi_buffer, n);
+ PVOID _new_buffer = realloc(_pi_buffer, n);
+ if (!isvalid(_new_buffer)) {
+ log_d("cannot reallocate process info buffer");
+ return -1;
+ }
+ _pi_buffer = _new_buffer;
return _py_thread__is_idle(self);
}
if (status != STATUS_SUCCESS) {
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..fb9488a9
--- /dev/null
+++ b/test/__init__.py
@@ -0,0 +1,9 @@
+import platform
+
+PY3_LATEST = 10
+
+match platform.system():
+ case "Darwin":
+ PYTHON_VERSIONS = [(3, _) for _ in range(7, PY3_LATEST + 1)]
+ case _:
+ PYTHON_VERSIONS = [(2, 7)] + [(3, _) for _ in range(5, PY3_LATEST + 1)]
diff --git a/test/common.bash b/test/common.bash
deleted file mode 100644
index 1adae87d..00000000
--- a/test/common.bash
+++ /dev/null
@@ -1,272 +0,0 @@
-# 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 .
-
-# -----------------------------------------------------------------------------
-# -- Austin
-# -----------------------------------------------------------------------------
-
-AUSTIN=`test -f src/austin && echo "src/austin" || echo "austin"`
-
-
-# -----------------------------------------------------------------------------
-# -- Python
-# -----------------------------------------------------------------------------
-
-function check_python {
- version="${1}"
-
- if ! python$version -V; then skip "Python $version not found."; fi
-
- PYTHON="python$version"
-}
-
-# -----------------------------------------------------------------------------
-# -- Logging
-# -----------------------------------------------------------------------------
-
-function log {
- echo "${1}" | tee -a "/tmp/austin_tests.log"
-}
-
-# -----------------------------------------------------------------------------
-
-function step {
- log " :: ${1}"
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Assertions
-# -----------------------------------------------------------------------------
-
-IGNORE=0
-FAIL=0
-REPEAT=0
-
-# -----------------------------------------------------------------------------
-
-function ignore {
- IGNORE=1
-}
-
-# -----------------------------------------------------------------------------
-
-function check_ignored {
- FAIL=1
-
- if [ $IGNORE == 1 ] && [ $REPEAT == 0 ]
- then
- log " The test it marked as 'ignore'"
- fi
- log
- log " Status: $status"
- log
- log " Collected Output"
- log " ================"
- log
- # for line in "${lines[@]}"
- # do
- # log " $line"
- # done
- log "$output"
- log
-
- if [ $IGNORE == 0 ] && [ $REPEAT == 0 ]; then false; fi
-}
-
-# -----------------------------------------------------------------------------
-
-function assert {
- local message="${1}"
- local condition="${2}"
-
- if ! eval "[[ $condition ]]"
- then
- log " Assertion failed: \"${message}\""
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_status {
- local estatus="${1}"
- : "${output?}"
- : "${status?}"
-
- assert "Got expected status (E: $estatus, G: $status)" "$status == $estatus"
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_success {
- : "${output?}"
- : "${status?}"
-
- assert "Command was successful" "$status == 0"
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_output {
- local pattern="${1}"
- : "${output?}"
-
- if ! echo "$output" | grep -q "${pattern}"
- then
- log " Assertion failed: Output contains pattern '${pattern}'"
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-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?}"
-
- if echo "$output" | grep -q "${pattern}"
- then
- log " Assertion failed: Output does not contain pattern '${pattern}'"
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_file {
- local file="$1"
- local pattern="${2}"
-
- if ! cat "$file" | grep -q "${pattern}"
- then
- log " Assertion failed: File $file contains pattern '${pattern}'"
- log
- log "File content"
- log "============"
- log
- log "$( head "$file" )"
- log ". . ."
- log "$( tail "$file" )"
- log
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_not_file {
- local file="$1"
- local pattern="${2}"
-
- if ! test -f $file
- then
- log " Assertion failed: File $file does not exist"
- check_ignored
- fi
-
- if cat "$file" | grep -q "${pattern}"
- then
- log " Assertion failed: File $file does not contain pattern '${pattern}'"
- log
- log "File content"
- log "============"
- log
- log "$( head "$file" )"
- log ". . ."
- log "$( tail "$file" )"
- log
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function repeat {
- local times="${1}"
- shift
-
- REPEAT=1
-
- for ((i=1;i<=times;i++))
- do
- log ">> Attempt $i of $times"
- FAIL=0
- $@
- if [ $FAIL == 0 ]; then return; fi
- done
-
- REPEAT=0
-
- log "<< Test failed on $times attempt(s)."
-
- if [ $IGNORE == 1 ]
- then
- skip "Failed but marked as 'ignore'."
- fi
-
- false
-}
diff --git a/test/macos/test.bats b/test/macos/test.bats
deleted file mode 100644
index 3b566779..00000000
--- a/test/macos/test.bats
+++ /dev/null
@@ -1,55 +0,0 @@
-# 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"
-
-
-test_case() {
- bats test/macos/test_$1.bats
-}
-
-
-@test "Test Austin: fork" {
- test_case fork
-}
-
-@test "Test Austin: fork multi-process" {
- test_case fork_mp
-}
-
-@test "Test Austin: attach" {
- test_case attach
-}
-
-@test "Test Austin: valgrind" {
- ignore
- if ! which valgrind; then skip "Valgrind not found"; fi
- test_case valgrind
-}
-
-@test "Test Austin: error messages" {
- test_case error
-}
-
-@test "Test Austin: sleepless" {
- test_case sleepless
-}
\ No newline at end of file
diff --git a/test/macos/test_attach.bats b/test/macos/test_attach.bats
deleted file mode 100644
index c2801497..00000000
--- a/test/macos/test_attach.bats
+++ /dev/null
@@ -1,77 +0,0 @@
-# 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 attach_austin {
- python_bin="${1}"
-
- if ! $python_bin -V; then skip "$python_bin not found."; fi
-
- log "Attach [Python $python_bin]"
-
- # -------------------------------------------------------------------------
- step "Time profiling"
- # -------------------------------------------------------------------------
- $python_bin test/sleepy.py &
- sleep 1
- run sudo $AUSTIN -i 10000 -t 100 -p $!
-
- assert_success
- assert_output "# austin: [[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
- assert_output ";.*test/sleepy.py::[[:digit:]]* "
-
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with default Python 3 from Homebrew" {
- attach_austin "/usr/local/bin/python3"
-}
-
-@test "Test Austin with Python 3.8 from Homebrew" {
- repeat 3 attach_austin "/usr/local/opt/python@3.8/bin/python3"
-}
-
-@test "Test Austin with Python 3.9 from Homebrew" {
- repeat 3 attach_austin "/usr/local/opt/python@3.9/bin/python3"
-}
-
-@test "Test Austin with Python 3.10 from Homebrew" {
- repeat 3 attach_austin "/usr/local/opt/python@3.10/bin/python3"
-}
-
-@test "Test Austin with Python 3 from Anaconda (if available)" {
- ignore
- repeat 3 attach_austin "/usr/local/anaconda3/bin/python"
-}
-
-# @test "Test Austin with the default Python 3" {
-# /usr/bin/python3 -m venv /tmp/py3
-# source /tmp/py3/bin/activate
-# attach_austin "python3"
-# test -d /tmp/py3 && rm -rf /tmp/py3
-# }
diff --git a/test/macos/test_error.bats b/test/macos/test_error.bats
deleted file mode 100644
index 7f17eb2a..00000000
--- a/test/macos/test_error.bats
+++ /dev/null
@@ -1,88 +0,0 @@
-# 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"
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test no arguments" {
- log "Test Austin with no arguments"
-
- run src/austin
-
- assert_success
- assert_output "Usage:"
-}
-
-@test "Test no command & PID" {
- log "Test Austin with no command nor PID"
-
- run src/austin -C
-
- assert_status 255
- assert_output "command to run or a PID"
-}
-
-@test "Test not Python" {
- log "Test Austin with a non-Python command"
-
- run src/austin cat
-
- assert_status 32 || assert_status 33
- assert_output "not a Python" || assert_output "Cannot launch"
-}
-
-@test "Test invalid command" {
- log "Test Austin with an invalid command"
-
- run src/austin snafubar
-
- assert_status 33
- assert_output "Cannot launch"
-}
-
-@test "Test invalid PID" {
- log "Test Austin with an invalid PID"
-
- run src/austin -p 9999999
-
- assert_status 36
- assert_output "Cannot attach"
-}
-
-@test "Test no permission" {
- log "Test Austin with no permissions"
-
- if [[ $EUID -eq 0 ]]; then
- skip "must not be root"
- fi
-
- python3 test/sleepy.py &
- sleep 1
- run src/austin -i 100ms -p $!
-
- assert_status 37
- assert_output "Insufficient permissions"
-}
diff --git a/test/macos/test_fork.bats b/test/macos/test_fork.bats
deleted file mode 100644
index 86dcae35..00000000
--- a/test/macos/test_fork.bats
+++ /dev/null
@@ -1,100 +0,0 @@
-# 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 {
- python_bin="${1}"
-
- if ! $python_bin -V; then skip "$python_bin not found."; fi
-
- log "Fork [Python $python_bin]"
-
- # -------------------------------------------------------------------------
- step "Standard profiling"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -i 1000 -t 10000 $python_bin test/target34.py
-
- assert_success
- assert_output "# austin: [[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
- assert_output ".*test/target34.py:keep_cpu_busy:32"
- assert_not_output "Unwanted"
-
- # -------------------------------------------------------------------------
- step "Memory profiling"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -i 1000 -t 10000 -m $python_bin test/target34.py
-
- assert_success
- assert_output ".*test/target34.py:keep_cpu_busy:32"
-
- # -------------------------------------------------------------------------
- step "Output file"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -i 10000 -t 10000 -o /tmp/austin_out.txt $python_bin test/target34.py
-
- assert_success
- assert_output "Unwanted"
- assert_not_output ".*test/target34.py:keep_cpu_busy:32"
- assert_file "/tmp/austin_out.txt" ".*test/target34.py:keep_cpu_busy:32"
-
-}
-
-# -----------------------------------------------------------------------------
-
-teardown() {
- if [ -f /tmp/austin_out.txt ]; then rm /tmp/austin_out.txt; fi
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with default Python 3 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/bin/python3"
-}
-
-@test "Test Austin with Python 3.8 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.8/bin/python3"
-}
-
-@test "Test Austin with Python 3.9 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.9/bin/python3"
-}
-
-@test "Test Austin with Python 3.10 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.10/bin/python3"
-}
-
-@test "Test Austin with Python 3 from Anaconda (if available)" {
- ignore
- repeat 3 invoke_austin "/usr/local/anaconda3/bin/python"
-}
-
-# @test "Test Austin with the default Python 3" {
-# /usr/bin/python3 -m venv --copies --without-pip /tmp/py3
-# source /tmp/py3/bin/activate
-# invoke_austin "python3"
-# test -d /tmp/py3 && rm -rf /tmp/py3
-# }
diff --git a/test/macos/test_fork_mp.bats b/test/macos/test_fork_mp.bats
deleted file mode 100644
index 072160d7..00000000
--- a/test/macos/test_fork_mp.bats
+++ /dev/null
@@ -1,80 +0,0 @@
-# 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 {
- python_bin="${1}"
-
- if ! $python_bin -V; then skip "$python_bin not found."; fi
-
- log "Fork Multi-processing [Python $python_bin]"
-
- # -------------------------------------------------------------------------
- step "Profiling of multi-process program"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -i 100000 -C $python_bin test/target_mp.py
-
- assert_success
-
- expected=3
- n_procs=$( echo "$output" | sed -E 's/P([0-9]+);.+/\1/' | sort | uniq | wc -l )
- assert "At least 3 parallel processes" "$n_procs -ge $expected"
-
- assert_output "# multiprocess: on"
- assert_output "fact"
-
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with default Python 3 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/bin/python3"
-}
-
-@test "Test Austin with Python 3.8 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.8/bin/python3"
-}
-
-@test "Test Austin with Python 3.9 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.9/bin/python3"
-}
-
-@test "Test Austin with Python 3.10 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.10/bin/python3"
-}
-
-@test "Test Austin with Python 3 from Anaconda (if available)" {
- ignore
- repeat 3 invoke_austin "/usr/local/anaconda3/bin/python"
-}
-
-# @test "Test Austin with the default Python 3" {
-# /usr/bin/python3 -m venv /tmp/py3
-# source /tmp/py3/bin/activate
-# invoke_austin "python3"
-# test -d /tmp/py3 && rm -rf /tmp/py3
-# }
diff --git a/test/macos/test_pipe.bats b/test/macos/test_pipe.bats
deleted file mode 100644
index 9b04b0e2..00000000
--- a/test/macos/test_pipe.bats
+++ /dev/null
@@ -1,92 +0,0 @@
-# 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 {
- python_bin="${1}"
-
- if ! $python_bin -V; then skip "$python_bin not found."; fi
-
- log "Pipe [Python $python_bin]"
-
- # -------------------------------------------------------------------------
- step "Test pipe output (wall time)"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -Pi 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# python: [[:digit:]]*.[[:digit:]]*."
- assert_output "# mode: wall"
- assert_output "# duration: [[:digit:]]*"
- assert_output "# interval: 100000"
-
- # -------------------------------------------------------------------------
- step "Test pipe output (CPU time)"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -Psi 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# python: [[:digit:]]*.[[:digit:]]*."
- assert_output "# mode: cpu"
- assert_output "# duration: [[:digit:]]*"
- assert_output "# interval: 100000"
-
- # -------------------------------------------------------------------------
- step "Test pipe output (multiprocess)"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -CPi 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# python: [[:digit:]]*.[[:digit:]]*."
- assert_output "# mode: wall"
- assert_output "# duration: [[:digit:]]*"
- assert_output "# interval: 100000"
- assert_output "# multiprocess: on"
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with default Python 3 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/bin/python3"
-}
-
-@test "Test Austin with Python 3.8 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.8/bin/python3"
-}
-
-@test "Test Austin with Python 3.9 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.9/bin/python3"
-}
-
-@test "Test Austin with Python 3.10 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.10/bin/python3"
-}
-
-@test "Test Austin with Python 3 from Anaconda (if available)" {
- ignore
- repeat 3 invoke_austin "/usr/local/anaconda3/bin/python"
-}
diff --git a/test/macos/test_sleepless.bats b/test/macos/test_sleepless.bats
deleted file mode 100644
index 42eb6664..00000000
--- a/test/macos/test_sleepless.bats
+++ /dev/null
@@ -1,67 +0,0 @@
-# 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 {
- python_bin="${1}"
-
- if ! $python_bin -V; then skip "$python_bin not found."; fi
-
- log "Sleepless [Python $python_bin]"
-
- # -------------------------------------------------------------------------
- step "Sleepless test"
- # -------------------------------------------------------------------------
- run sudo $AUSTIN -si 10ms -t 1s $python_bin test/sleepy.py
-
- assert_success
- assert_output ".*test/sleepy.py:cpu_bound:"
- assert_not_output ":35)"
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with default Python 3 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/bin/python3"
-}
-
-@test "Test Austin with Python 3.8 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.8/bin/python3"
-}
-
-@test "Test Austin with Python 3.9 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.9/bin/python3"
-}
-
-@test "Test Austin with Python 3.10 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.10/bin/python3"
-}
-
-@test "Test Austin with Python 3 from Anaconda (if available)" {
- ignore
- repeat 3 invoke_austin "/usr/local/anaconda3/bin/python"
-}
diff --git a/test/macos/test_valgrind.bats b/test/macos/test_valgrind.bats
deleted file mode 100644
index 88927309..00000000
--- a/test/macos/test_valgrind.bats
+++ /dev/null
@@ -1,87 +0,0 @@
-# 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 {
- python_bin="${1}"
-
- if ! $python_bin -V; then skip "$python_bin not found."; fi
-
- log "Valgrind [Python $python_bin]"
-
- # -------------------------------------------------------------------------
- step "Valgrind test"
- # -------------------------------------------------------------------------
- run valgrind \
- --error-exitcode=42 \
- --leak-check=full \
- --show-leak-kinds=all \
- --errors-for-leak-kinds=all \
- --track-fds=yes \
- $AUSTIN -i 100000 -t 10000 -o /dev/null $PYTHON test/target34.py
-
- if [ ! $status == 0 ]
- then
- log " Valgrind Report"
- log " ==============="
- for line in "${lines[@]}"
- do
- log " $line"
- done
- check_ignored
- fi
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with default Python 3 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/bin/python3"
-}
-
-@test "Test Austin with Python 3.8 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.8/bin/python3"
-}
-
-@test "Test Austin with Python 3.9 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.9/bin/python3"
-}
-
-@test "Test Austin with Python 3.10 from Homebrew" {
- repeat 3 invoke_austin "/usr/local/opt/python@3.10/bin/python3"
-}
-
-@test "Test Austin with Python 3 from Anaconda (if available)" {
- ignore
- repeat 3 invoke_austin "/usr/local/anaconda3/bin/python"
-}
-
-# @test "Test Austin with the default Python 3" {
-# /usr/bin/python3 -m venv /tmp/py3
-# source /tmp/py3/bin/activate
-# invoke_austin "python3"
-# test -d /tmp/py3 && rm -rf /tmp/py3
-# }
diff --git a/test/requirements.txt b/test/requirements.txt
new file mode 100644
index 00000000..8adc61a9
--- /dev/null
+++ b/test/requirements.txt
@@ -0,0 +1,2 @@
+pytest
+flaky
diff --git a/test/targets/recursive.py b/test/targets/recursive.py
new file mode 100644
index 00000000..cf43a8a1
--- /dev/null
+++ b/test/targets/recursive.py
@@ -0,0 +1,12 @@
+def sum_up_to(n):
+ if n <= 1:
+ return 1
+
+ result = n + sum_up_to(n - 1)
+
+ return result
+
+
+for _ in range(200000):
+ N = 16
+ assert sum_up_to(N) == (N * (N + 1)) >> 1
diff --git a/test/sleepy.py b/test/targets/sleepy.py
similarity index 89%
rename from test/sleepy.py
rename to test/targets/sleepy.py
index 32b040b5..4b258b67 100644
--- a/test/sleepy.py
+++ b/test/targets/sleepy.py
@@ -20,6 +20,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+import sys
import time
@@ -30,6 +31,11 @@ def cpu_bound():
if __name__ == "__main__":
+ try:
+ interval = float(sys.argv[1])
+ except IndexError:
+ interval = 0.7
+
for n in range(2):
cpu_bound()
- time.sleep(0.7)
+ time.sleep(interval)
diff --git a/test/target34.py b/test/targets/target34.py
similarity index 100%
rename from test/target34.py
rename to test/targets/target34.py
diff --git a/test/target_gc.py b/test/targets/target_gc.py
similarity index 100%
rename from test/target_gc.py
rename to test/targets/target_gc.py
diff --git a/test/target_mp.py b/test/targets/target_mp.py
similarity index 94%
rename from test/target_mp.py
rename to test/targets/target_mp.py
index 0e5b9ebf..eedaf6fa 100644
--- a/test/target_mp.py
+++ b/test/targets/target_mp.py
@@ -22,7 +22,6 @@
# source: https://lobste.rs/s/qairy5/austin_python_frame_stack_sampler_for
-import time
import multiprocessing
@@ -43,7 +42,7 @@ def do(N):
if __name__ == "__main__":
processes = []
for _ in range(2):
- process = multiprocessing.Process(target=do, args=(2000,))
+ process = multiprocessing.Process(target=do, args=(3000,))
process.start()
processes.append(process)
diff --git a/test/test.bats b/test/test.bats
deleted file mode 100644
index ac181976..00000000
--- a/test/test.bats
+++ /dev/null
@@ -1,56 +0,0 @@
-# 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"
-
-
-test_case() {
- bats test/test_$1.bats
-}
-
-@test "Test Austin: fork" {
- test_case fork
-}
-
-@test "Test Austin: fork multi-process" {
- test_case fork_mp
-}
-
-@test "Test Austin: attach" {
- if [[ $EUID -ne 0 ]]; then
- skip "requires root"
- fi
- test_case attach
-}
-
-@test "Test Austin: valgrind" {
- ignore
- test_case valgrind
-}
-
-@test "Test Austin: errors" {
- test_case error
-}
-
-@test "Test Austin: sleepless" {
- test_case sleepless
-}
\ No newline at end of file
diff --git a/test/win/test.bats b/test/test_accuracy.py
similarity index 52%
rename from test/win/test.bats
rename to test/test_accuracy.py
index d3d99177..9bf3306c 100644
--- a/test/win/test.bats
+++ b/test/test_accuracy.py
@@ -5,7 +5,7 @@
#
# Austin is a Python frame stack sampler for CPython.
#
-# Copyright (c) 2019 Gabriele N. Tornetta .
+# Copyright (c) 2022 Gabriele N. Tornetta .
# All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
@@ -20,21 +20,30 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-load "common"
+from test.utils import (
+ allpythons,
+ austin,
+ compress,
+ has_pattern,
+ python,
+ samples,
+ target,
+)
+import pytest
+from flaky import flaky
-test_case() {
- bats test/win/test_$1.bats
-}
-@test "Test Austin: fork" {
- test_case fork
-}
+@flaky
+@pytest.mark.parametrize("heap", [tuple(), ("-h", "0"), ("-h", "64")])
+@allpythons()
+def test_accuracy_fast_recursive(py, heap):
+ result = austin("-i", "1ms", *heap, *python(py), target("recursive.py"))
+ assert result.returncode == 0, result.stderr or result.stdout
-@test "Test Austin: fork multi-process" {
- test_case fork_mp
-}
+ assert has_pattern(result.stdout, "sum_up_to"), compress(result.stdout)
+ assert has_pattern(result.stdout, ":INVALID:"), compress(result.stdout)
-@test "Test Austin: errors" {
- test_case error
-}
+ for _ in samples(result.stdout):
+ if "sum_up_to" in _ and "" in _:
+ assert len(_.split(";")) <= 20, _
diff --git a/test/test_attach.bats b/test/test_attach.bats
deleted file mode 100644
index 920030cd..00000000
--- a/test/test_attach.bats
+++ /dev/null
@@ -1,106 +0,0 @@
-# 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 attach_austin {
- local version="${1}"
-
- check_python $version
-
- log "Attach [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Standard profiling"
- # -------------------------------------------------------------------------
- $PYTHON test/sleepy.py &
- sleep 1
- run $AUSTIN -i 10ms -t 100 -p $!
-
- assert_success
- assert_output "# austin: [[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
- assert_output ".*test/sleepy.py::[[:digit:]]* "
-
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python 2.3" {
- ignore
- repeat 3 attach_austin "2.3"
-}
-
-@test "Test Austin with Python 2.4" {
- ignore
- repeat 3 attach_austin "2.4"
-}
-
-@test "Test Austin with Python 2.5" {
- repeat 3 attach_austin "2.5"
-}
-
-@test "Test Austin with Python 2.6" {
- ignore "This test is known to fail"
- repeat 3 attach_austin "2.6"
-}
-
-@test "Test Austin with Python 2.7" {
- repeat 3 attach_austin "2.7"
-}
-
-@test "Test Austin with Python 3.3" {
- ignore "No longer tested"
- repeat 3 attach_austin "3.3"
-}
-
-@test "Test Austin with Python 3.4" {
- repeat 3 attach_austin "3.4"
-}
-
-@test "Test Austin with Python 3.5" {
- repeat 3 attach_austin "3.5"
-}
-
-@test "Test Austin with Python 3.6" {
- repeat 3 attach_austin "3.6"
-}
-
-@test "Test Austin with Python 3.7" {
- repeat 3 attach_austin "3.7"
-}
-
-@test "Test Austin with Python 3.8" {
- repeat 3 attach_austin "3.8"
-}
-
-@test "Test Austin with Python 3.9" {
- repeat 3 attach_austin "3.9"
-}
-
-
-@test "Test Austin with Python 3.10" {
- repeat 3 attach_austin "3.10"
-}
diff --git a/test/test_attach.py b/test/test_attach.py
new file mode 100644
index 00000000..ee7b85d7
--- /dev/null
+++ b/test/test_attach.py
@@ -0,0 +1,155 @@
+# 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) 2022 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 .
+
+import platform
+from collections import Counter
+from test.utils import allpythons as _allpythons
+from test.utils import (
+ austin,
+ austinp,
+ compress,
+ has_pattern,
+ metadata,
+ requires_sudo,
+ run_python,
+ sum_metric,
+ target,
+ threads,
+)
+from time import sleep
+
+import pytest
+from flaky import flaky
+
+
+def allpythons():
+ # Attach tests fail on Windows for Python < 3.7
+ return _allpythons(min=(3, 7) if platform.system() == "Windows" else None)
+
+
+@flaky(max_runs=3)
+@requires_sudo
+@pytest.mark.parametrize("heap", [tuple(), ("-h", "0"), ("-h", "64")])
+@pytest.mark.parametrize(
+ "mode,mode_meta", [("-i", "wall"), ("-si", "cpu"), ("-Ci", "wall"), ("-Csi", "cpu")]
+)
+@allpythons()
+def test_attach_wall_time(py, mode, mode_meta, heap):
+ with run_python(py, target("sleepy.py")) as p:
+ sleep(0.5)
+
+ result = austin(mode, f"10ms", *heap, "-p", str(p.pid))
+ assert result.returncode == 0
+
+ ts = threads(result.stdout)
+ assert len(ts) == 1, compress(result.stdout)
+
+ assert has_pattern(result.stdout, "sleepy.py::"), compress(
+ result.stdout
+ )
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == mode_meta
+
+ a = sum_metric(result.stdout)
+ d = int(meta["duration"])
+
+ assert a <= d
+
+
+@flaky
+@requires_sudo
+@pytest.mark.parametrize("exposure", [1, 2])
+@allpythons()
+def test_attach_exposure(py, exposure):
+ with run_python(py, target("sleepy.py"), "3") as p:
+ result = austin("-i", "1ms", "-x", str(exposure), "-p", str(p.pid))
+ assert result.returncode == 0
+
+ assert has_pattern(result.stdout, "sleepy.py::"), compress(
+ result.stdout
+ )
+
+ meta = metadata(result.stdout)
+
+ a = sum_metric(result.stdout)
+ d = int(meta["duration"])
+
+ assert exposure * 800000 <= d < exposure * 1200000
+
+ p.kill()
+
+
+@requires_sudo
+@allpythons()
+def test_where(py):
+ with run_python(py, target("sleepy.py")) as p:
+ sleep(1)
+ result = austin("-w", str(p.pid))
+ assert result.returncode == 0
+
+ assert "Process" in result.stdout
+ assert "Thread" in result.stdout
+ assert "sleepy.py" in result.stdout
+ assert "" in result.stdout
+
+
+@flaky
+@requires_sudo
+@pytest.mark.xfail(platform.system() == "Windows", reason="Does not pass in Windows CI")
+@allpythons()
+def test_where_multiprocess(py):
+ with run_python(py, target("target_mp.py")) as p:
+ while p.returncode is None:
+ sleep(0.2)
+ result = austin("-Cw", str(p.pid))
+ assert result.returncode == 0
+
+ lines = Counter(result.stdout.splitlines())
+
+ if sum(c for line, c in lines.items() if "Process" in line) >= 3:
+ break
+ else:
+ assert False, result.stdout
+
+ assert sum(c for line, c in lines.items() if "fact" in line) == 2, result.stdout
+ (join_line,) = (line for line in lines if "join" in line)
+ assert lines[join_line] == 1, result.stdout
+
+
+@flaky(max_runs=3)
+@requires_sudo
+@allpythons()
+def test_where_kernel(py):
+ with run_python(py, target("sleepy.py")) as p:
+ sleep(1)
+ result = austinp("-kw", str(p.pid))
+ assert result.returncode == 0
+
+ assert "Process" in result.stdout, result.stdout
+ assert "Thread" in result.stdout, result.stdout
+ assert "sleepy.py" in result.stdout, result.stdout
+ assert "" in result.stdout, result.stdout
+ assert "__select" in result.stdout, result.stdout
+ assert "libc" in result.stdout, result.stdout
+ assert "do_syscall" in result.stdout, result.stdout
diff --git a/test/test_cli.py b/test/test_cli.py
new file mode 100644
index 00000000..ff84a08d
--- /dev/null
+++ b/test/test_cli.py
@@ -0,0 +1,74 @@
+# 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) 2022 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 .
+
+import platform
+from test.utils import austin, no_sudo, run_python, target
+
+import pytest
+
+
+def test_cli_no_arguments():
+ result = austin()
+ assert result.returncode == 0
+ assert "Usage:" in result.stdout
+ assert not result.stderr
+
+
+def test_cli_no_python():
+ result = austin(
+ "-C",
+ "powershell" if platform.system() == "Windows" else "bash",
+ "-c",
+ "sleep 1",
+ )
+ if platform.system() == "Darwin":
+ # Darwin CI gives a different result than manual tests. We are accepting
+ # this for now.
+ assert result.returncode in (37, 39)
+ assert "Insufficient permissions" in result.stderr, result.stderr
+ else:
+ assert result.returncode == 39
+ assert "not a Python" in result.stderr or "Cannot launch" in result.stderr
+
+
+def test_cli_invalid_command():
+ result = austin("snafubar")
+ assert result.returncode == 33
+ assert "Cannot launch" in (result.stderr or result.stdout)
+
+
+def test_cli_invalid_pid():
+ result = austin("-p", "9999999")
+
+ assert result.returncode == 36
+ assert "Cannot attach" in result.stderr
+
+
+@pytest.mark.skipif(
+ platform.system() == "Windows", reason="No permission issues on Windows"
+)
+@no_sudo
+def test_cli_permissions():
+ with run_python("3", target("sleepy.py")) as p:
+ result = austin("-i", "1ms", "-p", str(p.pid))
+ assert result.returncode == 37, result.stderr
+ assert "Insufficient permissions" in result.stderr, result.stderr
diff --git a/test/test_error.bats b/test/test_error.bats
deleted file mode 100644
index 4028ed37..00000000
--- a/test/test_error.bats
+++ /dev/null
@@ -1,98 +0,0 @@
-# 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"
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test no arguments" {
- log "Test Austin with no arguments"
-
- run src/austin
-
- assert_success
- assert_output "Usage:"
-}
-
-@test "Test no command & PID" {
- log "Test Austin with no command nor PID"
-
- run src/austin -C
-
- assert_status 255
- assert_output "command to run or a PID"
-}
-
-@test "Test not Python" {
- skip "Unstable"
- log "Test Austin with a non-Python command"
-
- run src/austin cat
-
- assert_status 32
- assert_output "not a Python" || assert_output "Cannot launch"
-}
-
-@test "Test not Python nor Python children" {
- log "Test Austin with a non-Python command that spawns no Python children"
-
- run src/austin -C bash -c "sleep 1"
-
- assert_status 39
- assert_output "not a Python" || assert_output "Cannot launch"
-}
-
-@test "Test invalid command" {
- log "Test Austin with an invalid command"
-
- run src/austin snafubar
-
- assert_status 33
- assert_output "Cannot launch"
-}
-
-@test "Test invalid PID" {
- log "Test Austin with an invalid PID"
-
- run src/austin -p 9999999
-
- assert_status 36
- assert_output "Cannot attach"
-}
-
-@test "Test no permission" {
- log "Test Austin with no permissions"
-
- if [[ $EUID -eq 0 ]]; then
- skip "must not be root"
- fi
-
- python3 test/sleepy.py &
- sleep 1
- run src/austin -i 100ms -p $!
-
- assert_status 37
- assert_output "Insufficient permissions"
-}
diff --git a/test/test_fork.bats b/test/test_fork.bats
deleted file mode 100644
index f55b47a2..00000000
--- a/test/test_fork.bats
+++ /dev/null
@@ -1,129 +0,0 @@
-# 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 "Fork [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Standard profiling"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 1ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# austin: [[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
- assert_output ".*test/target34.py:keep_cpu_busy:32"
- assert_output "# duration: [[:digit:]]*"
- assert_not_output "Unwanted"
-
- # -------------------------------------------------------------------------
- step "Memory profiling"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 1000 -t 1000 -m $PYTHON test/target34.py
-
- assert_success
- assert_output "# memory: [[:digit:]]*"
- assert_output ".*test/target34.py:keep_cpu_busy:32"
-
- # -------------------------------------------------------------------------
- step "Output file"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 10000 -t 1000 -o /tmp/austin_out.txt $PYTHON test/target34.py
-
- assert_success
- assert_output "Unwanted"
- assert_not_output ".*test/target34.py:keep_cpu_busy:32"
- assert_file "/tmp/austin_out.txt" "# austin: [[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
- assert_file "/tmp/austin_out.txt" ".*test/target34.py:keep_cpu_busy:32"
-
-}
-
-# -----------------------------------------------------------------------------
-
-function teardown {
- if [ -f /tmp/austin_out.txt ]; then rm /tmp/austin_out.txt; fi
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python 2.3" {
- ignore
- repeat 3 invoke_austin "2.3"
-}
-
-@test "Test Austin with Python 2.4" {
- ignore
- repeat 3 invoke_austin "2.4"
-}
-
-@test "Test Austin with Python 2.5" {
- repeat 3 invoke_austin "2.5"
-}
-
-@test "Test Austin with Python 2.6" {
- repeat 3 invoke_austin "2.6"
-}
-
-@test "Test Austin with Python 2.7" {
- repeat 3 invoke_austin "2.7"
-}
-
-@test "Test Austin with Python 3.3" {
- repeat 3 invoke_austin "3.3"
-}
-
-@test "Test Austin with Python 3.4" {
- repeat 3 invoke_austin "3.4"
-}
-
-@test "Test Austin with Python 3.5" {
- repeat 3 invoke_austin "3.5"
-}
-
-@test "Test Austin with Python 3.6" {
- repeat 3 invoke_austin "3.6"
-}
-
-@test "Test Austin with Python 3.7" {
- repeat 3 invoke_austin "3.7"
-}
-
-@test "Test Austin with Python 3.8" {
- repeat 3 invoke_austin "3.8"
-}
-
-@test "Test Austin with Python 3.9" {
- repeat 3 invoke_austin "3.9"
-}
-
-@test "Test Austin with Python 3.10" {
- repeat 3 invoke_austin "3.10"
-}
diff --git a/test/test_fork.py b/test/test_fork.py
new file mode 100644
index 00000000..a80a264f
--- /dev/null
+++ b/test/test_fork.py
@@ -0,0 +1,220 @@
+# 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) 2022 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 .
+
+import platform
+from test.utils import (
+ allpythons,
+ austin,
+ compress,
+ has_pattern,
+ maps,
+ metadata,
+ processes,
+ python,
+ samples,
+ sum_metric,
+ sum_metrics,
+ target,
+ threads,
+ variants,
+)
+
+import pytest
+from flaky import flaky
+
+
+@flaky(max_runs=3)
+@pytest.mark.parametrize("heap", [tuple(), ("-h", "0"), ("-h", "64")])
+@allpythons()
+@variants
+def test_fork_wall_time(austin, py, heap):
+ result = austin("-i", "2ms", *heap, *python(py), target("target34.py"))
+ assert py in (result.stderr or result.stdout), result.stderr or result.stdout
+
+ assert len(processes(result.stdout)) == 1, compress(result.stdout)
+ ts = threads(result.stdout)
+ assert len(ts) == 2, compress(result.stdout)
+
+ assert has_pattern(result.stdout, "target34.py:keep_cpu_busy:3"), compress(
+ result.stdout
+ )
+ assert not has_pattern(result.stdout, "Unwanted")
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == "wall"
+
+ a = sum_metric(result.stdout)
+ d = int(meta["duration"])
+
+ assert 0 < a < 2.1 * d
+
+ if austin == "austinp":
+ ms = maps(result.stdout)
+ assert len(ms) >= 2, ms
+ assert [_ for _ in ms if "python" in _], ms
+
+
+@flaky
+@pytest.mark.parametrize("heap", [tuple(), ("-h", "0"), ("-h", "64")])
+@allpythons()
+def test_fork_cpu_time_cpu_bound(py, heap):
+ result = austin("-si", "1ms", *heap, *python(py), target("target34.py"))
+ assert result.returncode == 0, result.stderr or result.stdout
+
+ assert has_pattern(result.stdout, "target34.py:keep_cpu_busy:3"), compress(
+ result.stdout
+ )
+ assert not has_pattern(result.stdout, "Unwanted")
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == "cpu"
+
+ a = sum_metric(result.stdout)
+ d = int(meta["duration"])
+
+ assert 0 < a < 2.1 * d
+
+
+@flaky
+@allpythons()
+def test_fork_cpu_time_idle(py):
+ result = austin("-si", "1ms", *python(py), target("sleepy.py"))
+ assert result.returncode == 0, result.stderr or result.stdout
+
+ assert has_pattern(result.stdout, "sleepy.py::"), compress(result.stdout)
+
+ meta = metadata(result.stdout)
+
+ a = sum_metric(result.stdout)
+ d = int(meta["duration"])
+
+ assert a < 1.1 * d
+
+
+@flaky
+@allpythons()
+def test_fork_memory(py):
+ result = austin("-mi", "1ms", *python(py), target("target34.py"))
+ assert result.returncode == 0, result.stderr or result.stdout
+
+ assert has_pattern(result.stdout, "target34.py:keep_cpu_busy:32")
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == "memory"
+
+ d = int(meta["duration"])
+ assert d > 100000
+
+ ms = [int(_.rpartition(" ")[-1]) for _ in samples(result.stdout)]
+ alloc = sum(_ for _ in ms if _ > 0)
+ dealloc = sum(-_ for _ in ms if _ < 0)
+
+ assert alloc * dealloc
+
+
+@allpythons()
+def test_fork_output(py, tmp_path):
+ datafile = tmp_path / "test_fork_output.austin"
+
+ result = austin("-i", "1ms", "-o", datafile, *python(py), target("target34.py"))
+ assert result.returncode == 0, result.stderr or result.stdout
+
+ assert "Unwanted" in result.stdout
+
+ with datafile.open() as f:
+ data = f.read()
+ assert has_pattern(data, "target34.py:keep_cpu_busy:32")
+
+ meta = metadata(data)
+
+ assert meta["mode"] == "wall"
+
+ a = sum(int(_.rpartition(" ")[-1]) for _ in samples(data))
+ d = int(meta["duration"])
+
+ assert 0 < 0.9 * d < a < 2.1 * d
+
+
+# Support for multiprocess is attach-like and seems to suffer from the same
+# issues as attach tests on Windows.
+@flaky
+@pytest.mark.xfail(platform.system() == "Windows", reason="Does not pass in Windows CI")
+@allpythons(min=(3, 7) if platform.system() == "Windows" else None)
+def test_fork_multiprocess(py):
+ result = austin("-Ci", "1ms", *python(py), target("target_mp.py"))
+ assert result.returncode == 0, result.stderr or result.stdout
+
+ ps = processes(result.stdout)
+ assert len(ps) >= 3, ps
+
+ meta = metadata(result.stdout)
+ assert meta["multiprocess"] == "on", meta
+ assert meta["mode"] == "wall", meta
+
+ assert has_pattern(result.stdout, "target_mp.py:do:"), result.stdout
+ assert has_pattern(result.stdout, "target_mp.py:fact:"), result.stdout
+
+
+@flaky
+@allpythons()
+def test_fork_full_metrics(py):
+ result = austin("-i", "10ms", "-f", *python(py), target("target34.py"))
+ assert py in (result.stderr or result.stdout), result.stderr or result.stdout
+
+ assert len(processes(result.stdout)) == 1
+ ts = threads(result.stdout)
+ assert len(ts) == 2, ts
+
+ assert has_pattern(result.stdout, "target34.py:keep_cpu_busy:32")
+ assert not has_pattern(result.stdout, "Unwanted")
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == "full"
+
+ wall, cpu, alloc, dealloc = sum_metrics(result.stdout)
+ d = int(meta["duration"])
+
+ assert 0 < 0.9 * d < wall < 2.1 * d
+ assert 0 < cpu <= wall
+ assert alloc * dealloc
+
+
+@pytest.mark.parametrize("exposure", [1, 2])
+@allpythons()
+def test_fork_exposure(py, exposure):
+ result = austin(
+ "-i", "1ms", "-x", str(exposure), *python(py), target("sleepy.py"), "1"
+ )
+ assert result.returncode == 0, result.stderr or result.stdout
+
+ assert has_pattern(result.stdout, "sleepy.py::"), compress(result.stdout)
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == "wall"
+
+ d = int(meta["duration"])
+ assert 900000 * exposure < d < 1100000 * exposure
diff --git a/test/test_fork_mp.bats b/test/test_fork_mp.bats
deleted file mode 100644
index fddc6b50..00000000
--- a/test/test_fork_mp.bats
+++ /dev/null
@@ -1,107 +0,0 @@
-# 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 "Fork Multi-processing [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Profiling of multi-process program"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 100ms -C $PYTHON test/target_mp.py
-
- assert_success
-
- expected=3
- n_procs=$( echo "$output" | sed -r 's/P([0-9]+);.+/\1/' | sort | uniq | wc -l )
- assert "At least 3 parallel processes" "$n_procs -ge $expected"
-
- assert_output "# multiprocess: on"
- assert_output ".*test/target_mp.py:do:[[:digit:]]*;.*test/target_mp.py:fact:"
-
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python 2.3" {
- skip "Multiprocessing library introduced in Python 2.6"
- repeat 3 invoke_austin "2.3"
-}
-
-@test "Test Austin with Python 2.4" {
- skip "Multiprocessing library introduced in Python 2.6"
- repeat 3 invoke_austin "2.4"
-}
-
-@test "Test Austin with Python 2.5" {
- skip "Multiprocessing library introduced in Python 2.6"
- repeat 3 invoke_austin "2.5"
-}
-
-@test "Test Austin with Python 2.6" {
- repeat 3 invoke_austin "2.6"
-}
-
-@test "Test Austin with Python 2.7" {
- repeat 3 invoke_austin "2.7"
-}
-
-@test "Test Austin with Python 3.3" {
- repeat 3 invoke_austin "3.3"
-}
-
-@test "Test Austin with Python 3.4" {
- repeat 3 invoke_austin "3.4"
-}
-
-@test "Test Austin with Python 3.5" {
- repeat 3 invoke_austin "3.5"
-}
-
-@test "Test Austin with Python 3.6" {
- repeat 3 invoke_austin "3.6"
-}
-
-@test "Test Austin with Python 3.7" {
- repeat 3 invoke_austin "3.7"
-}
-
-@test "Test Austin with Python 3.8" {
- repeat 3 invoke_austin "3.8"
-}
-
-@test "Test Austin with Python 3.9" {
- repeat 3 invoke_austin "3.9"
-}
-
-@test "Test Austin with Python 3.10" {
- repeat 3 invoke_austin "3.10"
-}
diff --git a/test/test_gc.bats b/test/test_gc.bats
deleted file mode 100644
index bd774189..00000000
--- a/test/test_gc.bats
+++ /dev/null
@@ -1,86 +0,0 @@
-# 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"
-}
diff --git a/test/test_gc.py b/test/test_gc.py
new file mode 100644
index 00000000..0ea35bf4
--- /dev/null
+++ b/test/test_gc.py
@@ -0,0 +1,72 @@
+# 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) 2022 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 .
+
+import platform
+from test.utils import (
+ allpythons,
+ austin,
+ has_pattern,
+ metadata,
+ python,
+ samples,
+ target,
+)
+
+import pytest
+
+
+@allpythons(min=(3, 7))
+def test_gc_off(py):
+ result = austin("-i", "1ms", *python(py), target("target_gc.py"))
+ assert result.returncode == 0
+
+ assert not has_pattern(":GC:", result.stdout)
+
+
+@pytest.mark.xfail(
+ platform.system() != "Linux",
+ reason="GC sampling seems to work reliably only on Linux",
+)
+@allpythons(min=(3, 7))
+def test_gc_on(py):
+ result = austin("-gi", "1ms", *python(py), target("target_gc.py"))
+ assert result.returncode == 0
+
+ meta = metadata(result.stdout)
+ assert float(meta["gc"]) / float(meta["duration"]) > 0.1
+
+ gcs = [_ for _ in samples(result.stdout) if ":GC:" in _]
+ assert len(gcs) > 10
+
+
+@allpythons(min=(3, 7))
+def test_gc_disabled(py, monkeypatch):
+ monkeypatch.setenv("GC_DISABLED", "1")
+
+ result = austin("-gi", "10ms", *python(py), target("target_gc.py"))
+ assert result.returncode == 0
+
+ meta = metadata(result.stdout)
+ assert int(meta["gc"]) < int(meta["duration"]) / 20
+
+ gcs = [_ for _ in samples(result.stdout) if ":GC:" in _]
+ assert len(gcs) < 5
diff --git a/test/test_pipe.bats b/test/test_pipe.bats
deleted file mode 100644
index a5e9f8ea..00000000
--- a/test/test_pipe.bats
+++ /dev/null
@@ -1,124 +0,0 @@
-# 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 "Pipe [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Test pipe output (wall time)"
- # -------------------------------------------------------------------------
- run $AUSTIN -Pi 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# python: [[:digit:]]*.[[:digit:]]*."
- assert_output "# mode: wall"
- assert_output "# duration: [[:digit:]]*"
- assert_output "# interval: 100000"
-
- # -------------------------------------------------------------------------
- step "Test pipe output (CPU time)"
- # -------------------------------------------------------------------------
- run $AUSTIN -Psi 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# python: [[:digit:]]*.[[:digit:]]*."
- assert_output "# mode: cpu"
- assert_output "# duration: [[:digit:]]*"
- assert_output "# interval: 100000"
-
- # -------------------------------------------------------------------------
- step "Test pipe output (multiprocess)"
- # -------------------------------------------------------------------------
- run $AUSTIN -CPi 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# python: [[:digit:]]*.[[:digit:]]*."
- assert_output "# mode: wall"
- assert_output "# duration: [[:digit:]]*"
- assert_output "# interval: 100000"
- assert_output "# multiprocess: on"
-}
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python 2.3" {
- ignore
- repeat 3 invoke_austin "2.3"
-}
-
-@test "Test Austin with Python 2.4" {
- ignore
- repeat 3 invoke_austin "2.4"
-}
-
-@test "Test Austin with Python 2.5" {
- repeat 3 invoke_austin "2.5"
-}
-
-@test "Test Austin with Python 2.6" {
- repeat 3 invoke_austin "2.6"
-}
-
-@test "Test Austin with Python 2.7" {
- repeat 3 invoke_austin "2.7"
-}
-
-@test "Test Austin with Python 3.3" {
- repeat 3 invoke_austin "3.3"
-}
-
-@test "Test Austin with Python 3.4" {
- repeat 3 invoke_austin "3.4"
-}
-
-@test "Test Austin with Python 3.5" {
- repeat 3 invoke_austin "3.5"
-}
-
-@test "Test Austin with Python 3.6" {
- repeat 3 invoke_austin "3.6"
-}
-
-@test "Test Austin with Python 3.7" {
- repeat 3 invoke_austin "3.7"
-}
-
-@test "Test Austin with Python 3.8" {
- repeat 3 invoke_austin "3.8"
-}
-
-@test "Test Austin with Python 3.9" {
- repeat 3 invoke_austin "3.9"
-}
-
-@test "Test Austin with Python 3.10" {
- repeat 3 invoke_austin "3.10"
-}
diff --git a/test/test_pipe.py b/test/test_pipe.py
new file mode 100644
index 00000000..4c144abd
--- /dev/null
+++ b/test/test_pipe.py
@@ -0,0 +1,119 @@
+# 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) 2022 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 .
+
+from test.utils import (
+ allpythons,
+ austin,
+ compress,
+ has_pattern,
+ metadata,
+ processes,
+ python,
+ samples,
+ sum_metric,
+ target,
+ threads,
+)
+
+from flaky import flaky
+
+
+@flaky
+@allpythons()
+def test_pipe_wall_time(py):
+ interval = 1
+ result = austin("-Pi", f"{interval}ms", *python(py), target())
+ assert result.returncode == 0
+
+ meta = metadata(result.stdout)
+
+ assert meta["python"].startswith(py), meta
+ assert meta["mode"] == "wall", meta
+ assert int(meta["duration"]) > 100000, meta
+ assert meta["interval"] == str(interval * 1000), meta
+
+ assert len(processes(result.stdout)) == 1
+ ts = threads(result.stdout)
+ assert len(ts) == 2, ts
+
+ assert has_pattern(result.stdout, "target34.py:keep_cpu_busy:32")
+ assert not has_pattern(result.stdout, "Unwanted")
+
+ a = sum_metric(result.stdout)
+ d = int(meta["duration"])
+
+ assert 0 < 0.8 * d < a < 2.2 * d
+
+
+@allpythons()
+def test_pipe_cpu_time(py):
+ result = austin("-sPi", "1ms", *python(py), target())
+ assert result.returncode == 0
+
+ meta = metadata(result.stdout)
+
+ assert meta["python"].startswith(py), meta
+ assert meta["mode"] == "cpu", meta
+ assert int(meta["duration"]) > 100000, meta
+ assert meta["interval"] == "1000", meta
+
+
+@flaky(max_runs=3)
+@allpythons()
+def test_pipe_wall_time_multiprocess(py):
+ result = austin("-CPi", "1ms", *python(py), target())
+ assert result.returncode == 0
+
+ meta = metadata(result.stdout)
+
+ assert meta["mode"] == "wall", meta
+ assert int(meta["duration"]) > 100000, meta
+ assert meta["interval"] == "1000", meta
+ assert meta["multiprocess"] == "on", meta
+ assert meta["python"].startswith(py), meta
+
+
+@flaky
+@allpythons()
+def test_pipe_wall_time_multiprocess_output(py, tmp_path):
+ datafile = tmp_path / "test_pipe.austin"
+
+ result = austin("-CPi", "1ms", "-o", str(datafile), *python(py), target())
+ assert result.returncode == 0
+
+ with datafile.open() as f:
+ data = f.read()
+ meta = metadata(data)
+
+ assert meta, meta
+ assert meta["mode"] == "wall", meta
+ assert int(meta["duration"]) > 100000, meta
+ assert meta["interval"] == "1000", meta
+ assert meta["multiprocess"] == "on", meta
+ assert meta["python"].startswith(py), meta
+
+ assert has_pattern(data, "target34.py:keep_cpu_busy:32"), compress(data)
+
+ a = sum(int(_.rpartition(" ")[-1]) for _ in samples(data))
+ d = int(meta["duration"])
+
+ assert 0 < 0.8 * d < a < 2.2 * d
diff --git a/test/test_sleepless.bats b/test/test_sleepless.bats
deleted file mode 100644
index a36bc11c..00000000
--- a/test/test_sleepless.bats
+++ /dev/null
@@ -1,102 +0,0 @@
-# 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 "Sleepless [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Sleepless test"
- # -------------------------------------------------------------------------
- run $AUSTIN -si 100ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# mode: cpu"
- assert_output "test/target34.py:keep_cpu_busy:32"
- assert_not_output ":35"
-
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python 2.3" {
- ignore
- repeat 3 invoke_austin "2.3"
-}
-
-@test "Test Austin with Python 2.4" {
- ignore
- repeat 3 invoke_austin "2.4"
-}
-
-@test "Test Austin with Python 2.5" {
- repeat 3 invoke_austin "2.5"
-}
-
-@test "Test Austin with Python 2.6" {
- repeat 3 invoke_austin "2.6"
-}
-
-@test "Test Austin with Python 2.7" {
- repeat 3 invoke_austin "2.7"
-}
-
-@test "Test Austin with Python 3.3" {
- repeat 3 invoke_austin "3.3"
-}
-
-@test "Test Austin with Python 3.4" {
- repeat 3 invoke_austin "3.4"
-}
-
-@test "Test Austin with Python 3.5" {
- repeat 3 invoke_austin "3.5"
-}
-
-@test "Test Austin with Python 3.6" {
- repeat 3 invoke_austin "3.6"
-}
-
-@test "Test Austin with Python 3.7" {
- repeat 3 invoke_austin "3.7"
-}
-
-@test "Test Austin with Python 3.8" {
- repeat 3 invoke_austin "3.8"
-}
-
-@test "Test Austin with Python 3.9" {
- repeat 3 invoke_austin "3.9"
-}
-
-@test "Test Austin with Python 3.10" {
- repeat 3 invoke_austin "3.10"
-}
diff --git a/test/test_valgrind.bats b/test/test_valgrind.bats
deleted file mode 100644
index 73b7beb8..00000000
--- a/test/test_valgrind.bats
+++ /dev/null
@@ -1,135 +0,0 @@
-# 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 "Valgrind [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Valgrind wall test"
- # -------------------------------------------------------------------------
- run valgrind \
- --error-exitcode=42 \
- --leak-check=full \
- --show-leak-kinds=all \
- --errors-for-leak-kinds=all \
- --track-fds=yes \
- $AUSTIN -i 100ms -t 1s -o /dev/null $PYTHON test/target34.py
-
- if [ ! $status == 0 ]
- then
- log " Valgrind Report"
- log " ==============="
- for line in "${lines[@]}"
- do
- log " $line"
- done
- check_ignored
- fi
-
- # -------------------------------------------------------------------------
- step "Valgrind CPU test"
- # -------------------------------------------------------------------------
- run valgrind \
- --error-exitcode=42 \
- --leak-check=full \
- --show-leak-kinds=all \
- --errors-for-leak-kinds=all \
- --track-fds=yes \
- $AUSTIN -si 100 -t 1s -o /dev/null $PYTHON test/target34.py
-
- if [ ! $status == 0 ]
- then
- log " Valgrind Report"
- log " ==============="
- for line in "${lines[@]}"
- do
- log " $line"
- done
- check_ignored
- fi
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python 2.3" {
- ignore
- repeat 3 invoke_austin "2.3"
-}
-
-@test "Test Austin with Python 2.4" {
- ignore
- repeat 3 invoke_austin "2.4"
-}
-
-@test "Test Austin with Python 2.5" {
- repeat 3 invoke_austin "2.5"
-}
-
-@test "Test Austin with Python 2.6" {
- repeat 3 invoke_austin "2.6"
-}
-
-@test "Test Austin with Python 2.7" {
- repeat 3 invoke_austin "2.7"
-}
-
-@test "Test Austin with Python 3.3" {
- repeat 3 invoke_austin "3.3"
-}
-
-@test "Test Austin with Python 3.4" {
- repeat 3 invoke_austin "3.4"
-}
-
-@test "Test Austin with Python 3.5" {
- repeat 3 invoke_austin "3.5"
-}
-
-@test "Test Austin with Python 3.6" {
- repeat 3 invoke_austin "3.6"
-}
-
-@test "Test Austin with Python 3.7" {
- repeat 3 invoke_austin "3.7"
-}
-
-@test "Test Austin with Python 3.8" {
- repeat 3 invoke_austin "3.8"
-}
-
-@test "Test Austin with Python 3.9" {
- repeat 3 invoke_austin "3.9"
-}
-
-@test "Test Austin with Python 3.10" {
- repeat 3 invoke_austin "3.10"
-}
diff --git a/test/test_valgrind.py b/test/test_valgrind.py
new file mode 100644
index 00000000..dbde8975
--- /dev/null
+++ b/test/test_valgrind.py
@@ -0,0 +1,70 @@
+# 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) 2022 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 .
+
+from subprocess import run
+from test.utils import allpythons, austin, python, requires_sudo, run_python, target
+
+import pytest
+
+
+def valgrind(python_args: list[str], mode: str):
+ try:
+ return run(
+ [
+ "valgrind",
+ "--error-exitcode=42",
+ "--leak-check=full",
+ "--show-leak-kinds=all",
+ "--errors-for-leak-kinds=all",
+ "--track-fds=yes",
+ str(austin.path),
+ f"-{mode}i",
+ "1ms",
+ "-t",
+ "1s",
+ "-o",
+ "/dev/null",
+ *python_args,
+ ],
+ capture_output=True,
+ timeout=30,
+ text=True,
+ )
+ except FileNotFoundError:
+ pytest.skip("Valgrind not available")
+
+
+@pytest.mark.parametrize("mode", ["", "s", "C", "Cs"])
+@allpythons()
+def test_valgrind_fork(py, mode):
+ result = valgrind([*python(py), target()], mode)
+ assert result.returncode == 0, "\n".join((result.stdout, result.stderr))
+
+
+@requires_sudo
+@pytest.mark.parametrize("mode", ["", "s", "C", "Cs"])
+@allpythons()
+def test_valgrind_attach(py, mode):
+ with run_python(py, target("sleepy.py")) as p:
+ result = valgrind(["-p", str(p.pid)], mode)
+ assert result.returncode == 0, "\n".join((result.stdout, result.stderr))
+ p.kill()
diff --git a/test/utils.py b/test/utils.py
new file mode 100644
index 00000000..4a81029f
--- /dev/null
+++ b/test/utils.py
@@ -0,0 +1,233 @@
+# 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) 2022 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 .
+
+import os
+import platform
+from asyncio.subprocess import STDOUT
+from collections import Counter, defaultdict
+from pathlib import Path
+from subprocess import PIPE, CompletedProcess, Popen, check_output, run
+from test import PYTHON_VERSIONS
+from typing import Iterator, TypeVar
+
+import pytest
+
+HERE = Path(__file__).parent
+
+
+def target(name: str = "target34.py") -> str:
+ return str(HERE / "targets" / name)
+
+
+def allpythons(min=None, max=None):
+ def _(f):
+ versions = PYTHON_VERSIONS
+ if min is not None:
+ versions = [_ for _ in versions if _ >= min]
+ if max is not None:
+ versions = [_ for _ in versions if _ <= max]
+ return pytest.mark.parametrize(
+ "py", [".".join([str(_) for _ in v]) for v in versions]
+ )(f)
+
+ return _
+
+
+if platform.system() == "Darwin":
+ BREW_PREFIX = check_output(["brew", "--prefix"], text=True, stderr=STDOUT).strip()
+
+
+def python(version: str) -> list[str]:
+ match platform.system():
+ case "Windows":
+ py = ["py", f"-{version}"]
+ case "Darwin":
+ py = [f"{BREW_PREFIX}/opt/python@{version}/bin/python3"]
+ case "Linux":
+ py = [f"python{version}"]
+ case _:
+ raise RuntimeError(f"Unsupported platform: {platform.system()}")
+
+ try:
+ check_output([*py, "-V"], stderr=STDOUT)
+ return py
+ except FileNotFoundError:
+ pytest.skip(f"Python {version} not found")
+
+
+def gdb(cmds: list[str], *args: tuple[str]) -> str:
+ return check_output(
+ ["gdb"] + [_ for cs in (("-ex", _) for _ in cmds) for _ in cs] + list(args),
+ stderr=STDOUT,
+ ).decode()
+
+
+def bt(binary: Path) -> str:
+ if Path("core").is_file():
+ return gdb(["bt full", "q"], str(binary), "core")
+ return "No core dump available."
+
+
+EXEEXT = ".exe" if platform.system() == "Windows" else ""
+
+class Variant(str):
+
+ ALL: list["Variant"] = []
+
+ def __init__(self, name: str) -> None:
+ super().__init__()
+
+ austin_exe = f"{name}{EXEEXT}"
+ path = Path("src") / austin_exe
+ if not path.is_file():
+ path = Path(austin_exe)
+
+ self.path = path
+
+ self.ALL.append(self)
+
+ def __call__(self, *args: tuple[str], timeout: int = 60) -> CompletedProcess:
+ if not self.path.is_file():
+ pytest.skip(f"Variant '{self}' not available")
+
+ result = run(
+ [str(self.path)] + list(args),
+ capture_output=True,
+ timeout=timeout,
+ text=True,
+ errors="ignore",
+ )
+
+ if result.returncode in (-11, 139): # SIGSEGV
+ print(bt(self.path))
+
+ return result
+
+
+austin = Variant("austin")
+austinp = Variant("austinp")
+
+variants = pytest.mark.parametrize("austin", Variant.ALL)
+
+
+def run_async(command: list[str], *args: tuple[str]) -> Popen:
+ return Popen(command + list(args), stdout=PIPE, stderr=PIPE)
+
+
+def run_python(version, *args: tuple[str]) -> Popen:
+ return run_async(python(version), *args)
+
+
+def samples(data: str) -> Iterator[bytes]:
+ return (_ for _ in data.splitlines() if _ and _[0] == "P")
+
+
+T = TypeVar("T")
+
+def denoise(data: Iterator[T], threshold: float = 0.1) -> set[T]:
+ c = Counter(data)
+ try:
+ m = max(c.values())
+ except ValueError:
+ return set()
+ return {t for t, c in c.items() if c / m > threshold}
+
+
+def processes(data: str) -> set[str]:
+ return denoise(_.partition(";")[0] for _ in samples(data))
+
+
+def threads(data: str, threshold: float = 0.1) -> set[tuple[str, str]]:
+ return denoise(
+ tuple(_.rpartition(" ")[0].split(";", maxsplit=2)[:2]) for _ in samples(data)
+ )
+
+
+def metadata(data: str) -> dict[str, str]:
+ return dict(
+ _[1:].strip().split(": ", maxsplit=1)
+ for _ in data.splitlines()
+ if _ and _[0] == "#"
+ )
+
+
+def maps(data: str) -> defaultdict[str, list[str]]:
+ maps = defaultdict(list)
+ for r, f in (_[7:].split(" ", maxsplit=1) for _ in data.splitlines() if _.startswith("# map:")):
+ maps[f].append(r)
+ return maps
+
+
+def has_pattern(data: str, pattern: str) -> bool:
+ for _ in samples(data):
+ if pattern in _:
+ return True
+ return False
+
+
+def sum_metric(data: str) -> int:
+ return sum(int(_.rpartition(" ")[-1]) for _ in samples(data))
+
+
+def sum_metrics(data: str) -> tuple[int, int, int, int]:
+ wall = cpu = alloc = dealloc = 0
+ for t, i, m in (
+ _.rpartition(" ")[-1].split(",", maxsplit=2) for _ in samples(data)
+ ):
+ time = int(t)
+ wall += time
+ if i == "0":
+ cpu += time
+
+ memory = int(m)
+ if memory > 0:
+ alloc += memory
+ else:
+ dealloc += memory
+
+ return wall, cpu, alloc, dealloc
+
+
+def compress(data: str) -> str:
+ output: list[str] = []
+ stacks: dict[str, int] = {}
+ for _ in (_.strip() for _ in data.splitlines()):
+ if not _ or _[0] == "#":
+ output.append(_)
+ continue
+
+ stack, _, metric = _.rpartition(" ")
+ stacks[stack] = stacks.setdefault(stack, 0) + int(metric)
+
+ return "\n".join(output) + "\n".join((f"{k} {v}" for k, v in stacks.items()))
+
+
+match platform.system():
+ case "Windows":
+ requires_sudo = no_sudo = lambda f: f
+ case _:
+ requires_sudo = pytest.mark.skipif(
+ os.geteuid() != 0, reason="Requires superuser privileges"
+)
+ no_sudo = pytest.mark.skipif(
+ os.geteuid() == 0, reason="Must not have superuser privileges"
+ )
diff --git a/test/win/common.bash b/test/win/common.bash
deleted file mode 100644
index 3a99e58a..00000000
--- a/test/win/common.bash
+++ /dev/null
@@ -1,236 +0,0 @@
-# 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 .
-
-# -----------------------------------------------------------------------------
-# -- Austin
-# -----------------------------------------------------------------------------
-
-AUSTIN=`test -f src/austin.exe && echo "src/austin.exe" || echo "austin.exe"`
-
-
-# -----------------------------------------------------------------------------
-# -- Python
-# -----------------------------------------------------------------------------
-
-function check_python {
- if ! python -V; then skip "Python not found."; fi
-
- PYTHON="python"
-}
-
-# -----------------------------------------------------------------------------
-# -- Logging
-# -----------------------------------------------------------------------------
-
-function log {
- echo "${1}" | tee -a "/tmp/austin_tests.log"
-}
-
-# -----------------------------------------------------------------------------
-
-function step {
- log " :: ${1}"
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Assertions
-# -----------------------------------------------------------------------------
-
-IGNORE=0
-FAIL=0
-REPEAT=0
-
-# -----------------------------------------------------------------------------
-
-function ignore {
- IGNORE=1
-}
-
-# -----------------------------------------------------------------------------
-
-function check_ignored {
- FAIL=1
-
- if [ $IGNORE == 1 ] && [ $REPEAT == 0 ]
- then
- log " The test it marked as 'ignore'"
- fi
- log
- log " Status: $status"
- log
- log " Collected Output"
- log " ================"
- log
- # for line in "${lines[@]}"
- # do
- # log " $line"
- # done
- log "$output"
- log
-
- if [ $IGNORE == 0 ] && [ $REPEAT == 0 ]; then false; fi
-}
-
-# -----------------------------------------------------------------------------
-
-function assert {
- local message="${1}"
- local condition="${2}"
-
- if ! eval "[[ $condition ]]"
- then
- log " Assertion failed: \"${message}\""
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_status {
- local estatus="${1}"
- : "${output?}"
- : "${status?}"
-
- assert "Got expected status (E: $estatus, G: $status)" "$status == $estatus"
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_success {
- : "${output?}"
- : "${status?}"
-
- assert "Command was successful" "$status == 0"
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_output {
- local pattern="${1}"
- : "${output?}"
-
- if ! echo "$output" | grep -q "${pattern}"
- then
- log " Assertion failed: Output contains pattern '${pattern}'"
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_not_output {
- local pattern="${1}"
- : "${output?}"
-
- if echo "$output" | grep -q "${pattern}"
- then
- log " Assertion failed: Output does not contain pattern '${pattern}'"
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_file {
- local file="$1"
- local pattern="${2}"
-
- if ! cat "$file" | grep -q "${pattern}"
- then
- log " Assertion failed: File $file contains pattern '${pattern}'"
- log
- log "File content"
- log "============"
- log
- log "$( head "$file" )"
- log ". . ."
- log "$( tail "$file" )"
- log
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function assert_not_file {
- local file="$1"
- local pattern="${2}"
-
- if ! test -f $file
- then
- log " Assertion failed: File $file does not exist"
- check_ignored
- fi
-
- if cat "$file" | grep -q "${pattern}"
- then
- log " Assertion failed: File $file does not contain pattern '${pattern}'"
- log
- log "File content"
- log "============"
- log
- log "$( head "$file" )"
- log ". . ."
- log "$( tail "$file" )"
- log
- check_ignored
- fi
-
- true
-}
-
-# -----------------------------------------------------------------------------
-
-function repeat {
- local times="${1}"
- shift
-
- REPEAT=1
-
- for ((i=1;i<=times;i++))
- do
- log ">> Attempt $i of $times"
- FAIL=0
- $@
- if [ $FAIL == 0 ]; then return; fi
- done
-
- REPEAT=0
-
- log "<< Test failed on $times attempt(s)."
-
- if [ $IGNORE == 1 ]
- then
- skip "Failed but marked as 'ignore'."
- fi
-
- false
-}
diff --git a/test/win/test_error.bats b/test/win/test_error.bats
deleted file mode 100644
index dd5875fd..00000000
--- a/test/win/test_error.bats
+++ /dev/null
@@ -1,82 +0,0 @@
-# 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"
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test no arguments" {
- log "Test Austin with no arguments"
-
- run src/austin
-
- assert_success
- assert_output "Usage:"
-}
-
-@test "Test no command & PID" {
- log "Test Austin with no command nor PID"
-
- run src/austin -C
-
- assert_status 127
- assert_output "command to run or a PID"
-}
-
-@test "Test not Python" {
- log "Test Austin with a non-Python command"
-
- run src/austin cat
-
- assert_status 33
- assert_output "Cannot launch"
-}
-
-@test "Test not Python nor Python children" {
- log "Test Austin with a non-Python command that spawns no Python children"
-
- run src/austin -C cat
-
- assert_status 39
- assert_output "not a Python" || assert_output "Cannot launch"
-}
-
-@test "Test invalid command" {
- log "Test Austin with an invalid command"
-
- run src/austin snafubar
-
- assert_status 33
- assert_output "Cannot launch"
-}
-
-@test "Test invalid PID" {
- log "Test Austin with an invalid PID"
-
- run src/austin -p 9999999
-
- assert_status 36
- assert_output "Cannot attach"
-}
diff --git a/test/win/test_fork.bats b/test/win/test_fork.bats
deleted file mode 100644
index 4388cd34..00000000
--- a/test/win/test_fork.bats
+++ /dev/null
@@ -1,76 +0,0 @@
-# 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 "Fork [Python $version]"
-
- # -------------------------------------------------------------------------
- step "Standard profiling"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 1ms -t 1s $PYTHON test/target34.py
-
- assert_success
- assert_output "# austin: [[:digit:]]*.[[:digit:]]*.[[:digit:]]*"
- assert_output ".*test/target34.py:keep_cpu_busy:32"
- assert_not_output "Unwanted"
-
- # -------------------------------------------------------------------------
- step "Memory profiling"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 1000 -t 1000 -m $PYTHON test/target34.py
-
- assert_success
- assert_output ".*test/target34.py:keep_cpu_busy:32"
-
- # -------------------------------------------------------------------------
- step "Output file"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 10000 -t 1000 -o /tmp/austin_out.txt $PYTHON test/target34.py
-
- assert_success
- assert_output "Unwanted"
- assert_not_output ".*test/target34.py:keep_cpu_busy:32"
- assert_file "/tmp/austin_out.txt" ".*test/target34.py:keep_cpu_busy:32"
-
-}
-
-# -----------------------------------------------------------------------------
-
-function teardown {
- if [ -f /tmp/austin_out.txt ]; then rm /tmp/austin_out.txt; fi
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python" {
- repeat 3 invoke_austin
-}
diff --git a/test/win/test_fork_mp.bats b/test/win/test_fork_mp.bats
deleted file mode 100644
index ac14dcbd..00000000
--- a/test/win/test_fork_mp.bats
+++ /dev/null
@@ -1,53 +0,0 @@
-# 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 {
- check_python
-
- log "Fork Multi-processing [Python]"
-
- # -------------------------------------------------------------------------
- step "Profiling of multi-process program"
- # -------------------------------------------------------------------------
- run $AUSTIN -i 100ms -C $PYTHON test/target_mp.py
-
- assert_success
-
- expected=3
- n_procs=$( echo "$output" | sed -r 's/P([0-9]+);.+/\1/' | sort | uniq | wc -l )
- assert "At least 3 parallel processes" "$n_procs -ge $expected"
-
- assert_output "# multiprocess: on"
- assert_output ".*test[\\]target_mp.py:do:[[:digit:]]*;.*test[\\]target_mp.py:fact:"
-}
-
-
-# -----------------------------------------------------------------------------
-# -- Test Cases
-# -----------------------------------------------------------------------------
-
-@test "Test Austin with Python" {
- repeat 3 invoke_austin
-}
diff --git a/utils/resolve.py b/utils/resolve.py
index cf90d55f..1af9bd2d 100644
--- a/utils/resolve.py
+++ b/utils/resolve.py
@@ -17,9 +17,6 @@ def demangle_cython(function: str) -> str:
else:
raise ValueError(f"Invalid Cython mangled name: {function}")
- if function.startswith("__pyx_pf_"):
- function = function[: function.rindex(".isra.")]
-
n = 0
while i < len(function):
c = function[i]
@@ -83,15 +80,17 @@ def resolve(self, line: str) -> str:
parts = []
frames, _, metrics = line.strip().rpartition(" ")
for part in frames.split(";"):
- if part.startswith("native@"):
+ try:
head, function, lineno = part.split(":")
- if function.startswith("__pyx_pw_"):
- # skip Cython wrappers (cpdef)
- continue
- if function.startswith("__pyx_"):
- function = demangle_cython(function)
- elif function.startswith("_Z"):
- function = demangle_cpp(function)
+ except ValueError:
+ parts.append(part)
+ continue
+ if function.startswith("__pyx_pw_") or function.startswith("__pyx_pf_"):
+ # skip Cython wrappers (cpdef)
+ continue
+ if function.startswith("__pyx_"):
+ function = demangle_cython(function)
+ if head.startswith("native@"):
_, _, address = head.partition("@")
resolved = self.addr2line(address)
if resolved is None:
@@ -100,7 +99,7 @@ def resolve(self, line: str) -> str:
source, native_lineno = resolved
parts.append(f"{source}:{function}:{native_lineno or lineno}")
else:
- parts.append(part)
+ parts.append(":".join((head, function, lineno)))
return " ".join((";".join(parts), metrics))