diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 00000000..cbf6f5cb --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,106 @@ +name: Checks +on: push +jobs: + check-manpage: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Check manpage + steps: + - uses: actions/checkout@v2 + + - name: Install build dependencies + run: sudo apt-get -y install libunwind-dev binutils-dev libiberty-dev help2man + + - name: Compile Austin + run: | + autoreconf --install + ./configure + make + + - name: Generate manpage + run: bash doc/genman.sh + + - name: Check manpage + run: git diff --exit-code src/austin.1 + + cppcheck: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Static code analysis + steps: + - uses: actions/checkout@v2 + + - name: Install cppcheck + run: sudo apt-get -y install cppcheck + + - name: Check soure code + run: cppcheck -q -f --error-exitcode=1 src + + codespell: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Codespell + steps: + - uses: actions/checkout@v2 + + - uses: "actions/setup-python@v2" + with: + python-version: "3.9" + + - name: Install codespell + run: pip install codespell + + - name: Check source code spelling + run: codespell src + + coverage: + runs-on: ubuntu-latest + strategy: + fail-fast: false + name: Code coverage + steps: + - uses: actions/checkout@v2 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get -y install libunwind-dev binutils-dev libiberty-dev gcovr + + - name: Install test dependencies + run: | + sudo add-apt-repository -y ppa:deadsnakes/ppa + sudo apt-get -y install \ + valgrind \ + python2.7 \ + python3.{5..10} \ + python3.10-full + + - name: Compile Austin + run: | + autoreconf --install + ./configure --enable-coverage + make + + - name: Run tests + run: | + ulimit -c unlimited + python3.10 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r test/requirements.txt + .venv/bin/pytest --pastebin=failed -svvr fE || true + sudo .venv/bin/pytest --pastebin=failed -svvr fE || true + deactivate + + - name: Generate Cobertura report + run: gcovr --xml ./cobertura.xml -r src/ + + - name: Upload report to Codecov + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./cobertura.xml + verbose: true diff --git a/.github/workflows/dev_release.yml b/.github/workflows/pre_release.yml similarity index 80% rename from .github/workflows/dev_release.yml rename to .github/workflows/pre_release.yml index c385861e..15d4d537 100644 --- a/.github/workflows/dev_release.yml +++ b/.github/workflows/pre_release.yml @@ -1,8 +1,8 @@ -name: Development release +name: Pre-release on: push: tags: - - 'dev' + - 'v*-*' jobs: release-linux: runs-on: ubuntu-latest @@ -16,7 +16,7 @@ jobs: - name: Generate artifacts run: | sudo apt-get update - sudo apt-get -y install autoconf build-essential libunwind-dev musl-tools + sudo apt-get -y install autoconf build-essential libunwind-dev binutils-dev libiberty-dev musl-tools # Build austin autoreconf --install @@ -25,12 +25,9 @@ jobs: # Compute dev version export PREV_VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p") - export VERSION=$(echo $PREV_VERSION | awk -F. '{A=NF-1; $A = $A + 1; $NF=0} 1' | sed 's/ /./g')-dev+$(git rev-parse --short HEAD) + export VERSION=${{ github.ref_name }} sed -i "s/$PREV_VERSION/$VERSION/g" src/austin.h - # Build austinp - gcc -O3 -Os -s -Wall -pthread src/*.c -o src/austinp -DAUSTINP -l:libunwind-ptrace.a -l:liblzma.a -l:libunwind-generic.a -l:libunwind.a - pushd src tar -Jcf austin-$VERSION-gnu-linux-amd64.tar.xz austin tar -Jcf austinp-$VERSION-gnu-linux-amd64.tar.xz austinp @@ -46,11 +43,11 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: src/austin*xz + file: src/austin*.tar.xz tag: ${{ github.ref }} overwrite: true prerelease: true - release_name: Development build + release_name: ${{ github.ref_name }} file_glob: true release-win: @@ -72,8 +69,8 @@ jobs: # Compute dev version export PREV_VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p") - export VERSION=$(echo $PREV_VERSION | awk -F. '{A=NF-1; $A = $A + 1; $NF=0} 1' | sed 's/ /./g') - export VERSION_DEV=$(echo $PREV_VERSION | awk -F. '{A=NF-1; $A = $A + 1; $NF=0} 1' | sed 's/ /./g')-dev+$(git rev-parse --short HEAD) + export VERSION_DEV=${{ github.ref_name }} + export VERSION=$(echo $PREV_VERSION | sed -r -n "s/([0-9]+\.[0-9]+\.[0-9]+).*/\1/p") sed -i "s/$PREV_VERSION/$VERSION/g" src/austin.h gcc -s -Wall -O3 -Os -o src/austin src/*.c -lpsapi -lntdll @@ -107,7 +104,7 @@ jobs: tag: ${{ github.ref }} overwrite: true prerelease: true - release_name: Development build + release_name: ${{ github.ref_name }} file_glob: true release-osx: @@ -123,7 +120,7 @@ jobs: run: | # Compute dev version export PREV_VERSION=$(cat src/austin.h | sed -n -E "s/.*VERSION[ ]+\"(.+)\"/\1/p") - export VERSION=$(echo $PREV_VERSION | awk -F. '{A=NF-1; $A = $A + 1; $NF=0} 1' | sed 's/ /./g')-dev+$(git rev-parse --short HEAD) + export VERSION=${{ github.ref_name }} sed -i "" "s/$PREV_VERSION/$VERSION/g" src/austin.h echo "::set-output name=version::$VERSION" @@ -141,5 +138,6 @@ jobs: tag: ${{ github.ref }} overwrite: true prerelease: true - release_name: Development build - file_glob: true \ No newline at end of file + release_name: ${{ github.ref_name }} + body: See the changelog for details. + file_glob: true diff --git a/.github/workflows/dev_release_arch.yml b/.github/workflows/pre_release_arch.yml similarity index 80% rename from .github/workflows/dev_release_arch.yml rename to .github/workflows/pre_release_arch.yml index d6e7a49f..1cb6616e 100644 --- a/.github/workflows/dev_release_arch.yml +++ b/.github/workflows/pre_release_arch.yml @@ -1,8 +1,8 @@ -name: Development release (Linux archs) +name: Pre-release (Linux archs) on: push: tags: - - 'dev' + - 'v*-*' jobs: release-linux-archs: runs-on: ubuntu-latest @@ -27,11 +27,11 @@ jobs: # Compute dev version export PREV_VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p") - export VERSION=$(echo $PREV_VERSION | awk -F. '{A=NF-1; $A = $A + 1; $NF=0} 1' | sed 's/ /./g')-dev+$(git rev-parse --short HEAD) + export VERSION=${{ github.ref_name }} sed -i "s/$PREV_VERSION/$VERSION/g" src/austin.h run: | apt-get update - apt-get -y install autoconf build-essential libunwind-dev musl-tools + apt-get -y install autoconf build-essential libunwind-dev binutils-dev libiberty-dev musl-tools # Build austin autoreconf --install @@ -40,9 +40,6 @@ jobs: export VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p") - # Build austinp - gcc -O3 -Os -s -Wall -pthread src/*.c -o src/austinp -DAUSTINP -l:libunwind-ptrace.a -l:liblzma.a -l:libunwind-generic.a -l:libunwind.a - pushd src tar -Jcf austin-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austin tar -Jcf austinp-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austinp @@ -63,9 +60,10 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: artifacts/austin* + file: artifacts/austin*.tar.xz tag: ${{ github.ref }} overwrite: true prerelease: true - release_name: Development build - file_glob: true \ No newline at end of file + release_name: ${{ github.ref_name }} + body: See the changelog for details. + file_glob: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a48562e..a939fd0b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: tags: - - 'v*' + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release-linux: runs-on: ubuntu-latest @@ -16,7 +16,7 @@ jobs: - name: Generate artifacts run: | sudo apt-get update - sudo apt-get -y install autoconf build-essential libunwind-dev musl-tools + sudo apt-get -y install autoconf build-essential libunwind-dev binutils-dev libiberty-dev musl-tools # Build austin autoreconf --install @@ -25,9 +25,6 @@ jobs: export VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p"); - # Build austinp - gcc -O3 -Os -s -Wall -pthread src/*.c -o src/austinp -DAUSTINP -l:libunwind-ptrace.a -l:liblzma.a -l:libunwind-generic.a -l:libunwind.a - pushd src tar -Jcf austin-$VERSION-gnu-linux-amd64.tar.xz austin tar -Jcf austinp-$VERSION-gnu-linux-amd64.tar.xz austinp @@ -43,7 +40,7 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: src/austin-* + file: src/austin-*.tar.xz tag: ${{ github.ref }} overwrite: true file_glob: true @@ -139,4 +136,4 @@ jobs: file: src/austin-* tag: ${{ github.ref }} overwrite: true - file_glob: true \ No newline at end of file + file_glob: true diff --git a/.github/workflows/release_arch.yml b/.github/workflows/release_arch.yml index 832116cd..edd6980d 100644 --- a/.github/workflows/release_arch.yml +++ b/.github/workflows/release_arch.yml @@ -2,7 +2,7 @@ name: Release (Linux archs) on: push: tags: - - 'v*' + - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release-linux-archs: runs-on: ubuntu-latest @@ -26,7 +26,7 @@ jobs: mkdir -p ./artifacts run: | apt-get update - apt-get -y install autoconf build-essential libunwind-dev musl-tools + apt-get -y install autoconf build-essential libunwind-dev binutils-dev libiberty-dev musl-tools # Build austin autoreconf --install @@ -35,9 +35,6 @@ jobs: export VERSION=$(cat src/austin.h | sed -r -n "s/.*VERSION[ ]+\"(.+)\"/\1/p") - # Build austinp - gcc -O3 -Os -s -Wall -pthread src/*.c -o src/austinp -DAUSTINP -l:libunwind-ptrace.a -l:liblzma.a -l:libunwind-generic.a -l:libunwind.a - pushd src tar -Jcf austin-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austin tar -Jcf austinp-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austinp @@ -58,7 +55,7 @@ jobs: uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: artifacts/austin-* + file: artifacts/austin-*.tar.xz tag: ${{ github.ref }} overwrite: true - file_glob: true \ No newline at end of file + file_glob: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee31e2b4..2be0fab8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,5 +1,8 @@ name: Tests on: push +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true jobs: tests-linux: runs-on: ubuntu-latest @@ -8,26 +11,37 @@ jobs: name: Tests on Linux steps: - uses: actions/checkout@v2 - - name: Compile Austin + + - name: Install build dependencies run: | - autoreconf --install - ./configure - make + sudo apt-get -y install libunwind-dev binutils-dev libiberty-dev - name: Install test dependencies run: | sudo add-apt-repository -y ppa:deadsnakes/ppa - sudo apt-get -y install valgrind python2.{3..7} python3.{3..10} - npm install bats + sudo apt-get -y install \ + valgrind \ + gdb \ + python2.7 \ + python3.{5..10} \ + python3.10-full - - name: Run tests - run: sudo node_modules/.bin/bats test/test.bats + - name: Compile Austin + run: | + autoreconf --install + ./configure --enable-debug-symbols true + make - - name: Show test logs + - name: Run tests run: | - test -f /tmp/austin_tests.log && cat /tmp/austin_tests.log || true - test -f ./test-suite.log && cat ./test-suite.log || true - if: always() + ulimit -c unlimited + python3.10 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r test/requirements.txt + .venv/bin/pytest --pastebin=failed -svvr a + sudo .venv/bin/pytest --pastebin=failed -svvr a + deactivate tests-osx: runs-on: macos-latest @@ -37,24 +51,24 @@ jobs: steps: - uses: actions/checkout@v2 - name: Compile Austin - run: gcc -Wall -O3 -Os src/*.c -o src/austin -DDEBUG + run: gcc -Wall -O3 -g src/*.c -o src/austin - name: Install test dependencies run: | brew update - brew install python || brew upgrade python - brew install python@3.8 || true - brew install python@3.9 || true - brew install python@3.10 || true - brew install bats-core || true - brew install --cask anaconda || true + brew install python@3.7 + brew install python@3.8 + brew install python@3.9 + brew install python@3.10 - name: Run tests - run: sudo bats test/macos/test.bats - - - name: Show test logs - run: test -f /tmp/austin_tests.log && cat /tmp/austin_tests.log - if: always() + run: | + $(brew --prefix)/opt/python@3.10/bin/python3 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r test/requirements.txt + sudo .venv/bin/pytest --pastebin=failed -svvr a + deactivate tests-win: runs-on: windows-latest @@ -63,19 +77,32 @@ jobs: name: Tests on Windows steps: - uses: actions/checkout@v2 + - name: Compile Austin run: | - gcc.exe -O3 -o src/austin.exe src/*.c -lpsapi -lntdll -Wall -Os -s -DDEBUG - src\austin.exe --usage + gcc.exe -O3 -g -o src/austin.exe src/*.c -lpsapi -lntdll -Wall + src\austin.exe --help + + - uses: actions/setup-python@v2 + name: Setup Python + with: + python-version: '3.10' - name: Install test dependencies run: | - git clone --depth=1 https://github.com/bats-core/bats-core.git + choco install python --version=2.7.11 --no-progress + choco install python --version=3.5.4 --no-progress + choco install python --version=3.6.8 --no-progress + choco install python --version=3.7.9 --no-progress + choco install python --version=3.8.10 --no-progress + choco install python --version=3.9.10 --no-progress + refreshenv - name: Run tests run: | - bats-core/bin/bats test/win/test.bats - - - name: Show austin logs - run: test -f /tmp/austin.log && cat /tmp/austin.log || true - if: always() + py -3.10 -m venv venv + venv\Scripts\Activate.ps1 + python -m pip install --upgrade pip + python -m pip install -r test/requirements.txt + python -m pytest --pastebin=failed -svvr a + deactivate diff --git a/.gitignore b/.gitignore index a65859ed..b283606c 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,9 @@ venv.bak/ debian/austin debian/files *.substvars + + +# ----------------------------------------------------------------------------- +# -- EDITORS +# ----------------------------------------------------------------------------- +.vscode diff --git a/ChangeLog b/ChangeLog index 1e8cd11d..7e1177fc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,24 @@ -2021-xx-xx v3.2.0 +2022-01-28 v3.3.0 + + Added the new Where mode. + + Overall performance and accuracy improvements. + + The heap allocation has been turned off by default. + + Added support for profiling child processes that might have a different Python + runtime version than the parent process. + + Bugfix: fixed heap size handling. + + Bugfix: fixed a potential segmentation fault issue in the austinp variant. + + Bugfix: fixed a potential deadlock scenario in austinp. + + Bugfix: fixed support for the py.exe launcher on Windows. + + +2021-12-16 v3.2.0 Improved detection of invalid samples diff --git a/Dockerfile b/Dockerfile index 3d9798f1..3d283227 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:20.04 COPY . /austin RUN apt-get update && \ - apt-get install -y autoconf build-essential && \ + apt-get install -y autoconf build-essential libunwind-dev binutils-dev libiberty-dev && \ cd /austin && \ autoreconf --install && \ ./configure && \ diff --git a/Makefile.am b/Makefile.am index 5367279c..92acc8c4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,6 +1,2 @@ SUBDIRS = \ src - -TESTS = test/test.bats -TEST_EXTENSIONS = .bats -BATS_LOG_COMPILER = bats diff --git a/README.md b/README.md index 6627ccb9..97048b7b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ GitHub Action Status: Tests + + GitHub Action Status: Checks + + + Code coverage + latest release @@ -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 + +

+ Austin where mode example +

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

+ Austin where mode 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))