Skip to main content

py-spy: Sampling profiler for Python programs

Build Status

This fork adds read-only inspection of live asyncio tasks, including their suspended frames and creation tracebacks. The upstream project is benfred/py-spy.

py-spy is a sampling profiler for Python programs. It lets you visualize what your Python program is spending time on without restarting the program or modifying the code in any way. py-spy is extremely low overhead: it is written in Rust for speed and doesn't run in the same process as the profiled Python program. This means py-spy is safe to use against production Python code.

py-spy works on Linux, OSX, Windows and FreeBSD, and supports profiling all recent versions of the CPython interpreter (versions 2.3-2.7 and 3.3-3.14).

Installation

To build this fork with asyncio task inspection:

git clone https://github.com/wuurrd/py-spy.git
cd py-spy
cargo build --release
./target/release/py-spy dump --asyncio --pid 12345

The packages and binaries below are maintained by the upstream project and do not include this fork's asyncio additions.

Prebuilt binary wheels can be installed from PyPI with:

pip install py-spy

You can also download prebuilt binaries from the GitHub Releases Page.

If you're a Rust user, py-spy can also be installed with: cargo install py-spy. Note this builds py-spy from source and requires libunwind on Linux and Window, e.g., apt install libunwind-dev.

On macOS, py-spy is in Homebrew and can be installed with brew install py-spy.

On Arch Linux, py-spy is in AUR and can be installed with yay -S py-spy.

On Alpine Linux, py-spy is in testing repository and can be installed with apk add py-spy --update-cache --repository http://dl-3.alpinelinux.org/alpine/edge/testing/ --allow-untrusted.

Usage

py-spy works from the command line and takes either the PID of the program you want to sample from or the command line of the python program you want to run. py-spy has three subcommands record, top and dump:

record

py-spy supports recording profiles to a file using the record command. For example, you can generate a flame graph of your python process by going:

py-spy record -o profile.svg --pid 12345
# OR
py-spy record -o profile.svg -- python myprogram.py

Which will generate an interactive SVG file looking like:

flame graph

You can change the file format to generate speedscope profiles or raw data with the --format parameter. See py-spy record --help for information on other options including changing the sampling rate, filtering to only include threads that hold the GIL, profiling native C extensions, showing thread-ids, profiling subprocesses and more.

top

Top shows a live view of what functions are taking the most time in your python program, similar to the Unix top command. Running py-spy with:

py-spy top --pid 12345
# OR
py-spy top -- python myprogram.py

will bring up a live updating high level view of your python program:

console viewer demo

dump

py-spy can also display the current call stack for each python thread with the dump command:

py-spy dump --pid 12345

This will dump out the call stacks for each thread, and some other basic process info to the console:

dump output

This is useful for the case where you just need a single call stack to figure out where your python program is hung on. This command also has the ability to print out the local variables associated with each stack frame by setting the --locals flag.

Asyncio tasks

This fork can also inspect every live asyncio task without running code in the target process:

py-spy dump --asyncio --pid 12345

To omit tasks that have already finished:

py-spy dump --asyncio --asyncio-hide-finished --pid 12345

The additional Asyncio Tasks section includes each task's id, name, state, the frame where its coroutine is currently running or suspended, and its creation traceback when available. CPython only records creation tracebacks for tasks created while asyncio debug mode is enabled (for example with PYTHONASYNCIODEBUG=1, asyncio.run(..., debug=True), or loop.set_debug(True)). It works with CPython 3.7 through 3.14 and can be combined with --locals. With --asyncio --json, the output is an object containing threads and asyncio_tasks; JSON output without --asyncio retains py-spy's original array format.

Frequently Asked Questions

Why do we need another Python profiler?

This project aims to let you profile and debug any running Python program, even if the program is serving production traffic.

While there are many other python profiling projects, almost all of them require modifying the profiled program in some way. Usually, the profiling code runs inside of the target python process, which will slow down and change how the program operates. This means it's not generally safe to use these profilers for debugging issues in production services since they will usually have a noticeable impact on performance.

How does py-spy work?

py-spy works by directly reading the memory of the python program using the process_vm_readv system call on Linux, the vm_read call on OSX or the ReadProcessMemory call on Windows.

Figuring out the call stack of the Python program is done by looking at the global PyInterpreterState variable to get all the Python threads running in the interpreter, and then iterating over each PyFrameObject in each thread to get the call stack. Since the Python ABI changes between versions, we use rust's bindgen to generate different rust structures for each Python interpreter class we care about and use these generated structs to figure out the memory layout in the Python program.

Getting the memory address of the Python Interpreter can be a little tricky due to Address Space Layout Randomization. If the target python interpreter ships with symbols it is pretty easy to figure out the memory address of the interpreter by dereferencing the interp_head or _PyRuntime variables depending on the Python version. However, many Python versions are shipped with either stripped binaries or shipped without the corresponding PDB symbol files on Windows. In these cases we scan through the BSS section for addresses that look like they may point to a valid PyInterpreterState and check if the layout of that address is what we expect.

Can py-spy profile native extensions?

Yes! py-spy supports profiling native python extensions written in languages like C/C++ or Cython, on some platforms (see table below). You can enable this mode by passing --native on the command line. For best results, you should compile your Python extension with symbols. Also worth noting for Cython programs is that py-spy needs the generated C or C++ file in order to return line numbers of the original .pyx file. Read the blog post for more information.

Linux Windows OSX FreeBSD
i686
x86-64 yes yes
ARM yes
Aarch64 yes

How can I profile subprocesses?

By passing in the --subprocesses flag to either the record or top view, py-spy will also include the output from any python process that is a child process of the target program. This is useful for profiling applications that use multiprocessing or gunicorn worker pools. py-spy will monitor for new processes being created, and automatically attach to them and include samples from them in the output. The record view will include the PID and cmdline of each program in the callstack, with subprocesses appearing as children of their parent processes.

When do you need to run as sudo?

py-spy works by reading memory from a different python process, and this might not be allowed for security reasons depending on your OS and system settings. In many cases, running as a root user (with sudo or similar) gets around these security restrictions. OSX always requires running as root, but on Linux it depends on how you are launching py-spy and the system security settings.

On Linux the default configuration is to require root permissions when attaching to a process that isn't a child. For py-spy this means you can profile without root access by getting py-spy to create the process (py-spy record -- python myprogram.py) but attaching to an existing process by specifying a PID will usually require root (sudo py-spy record --pid 123456). You can remove this restriction on Linux by setting the ptrace_scope sysctl variable.

How do you detect if a thread is idle or not?

py-spy attempts to only include stack traces from threads that are actively running code, and exclude threads that are sleeping or otherwise idle. When possible, py-spy attempts to get this thread activity information from the OS: by reading in /proc/PID/stat on Linux, by using the mach thread_basic_info call on OSX, and by looking if the current SysCall is known to be idle on Windows.

There are some limitations with this approach though that may cause idle threads to still be marked as active. First off, we have to get this thread activity information before pausing the program, because getting this from a paused program will cause it to always return that this is idle. This means there is a potential race condition, where we get the thread activity and then the thread is in a different state when we get the stack trace. Querying the OS for thread activity also isn't implemented yet for FreeBSD and i686/ARM processors on Linux. On Windows, calls that are blocked on IO also won't be marked as idle yet, for instance when reading input from stdin. Finally, on some Linux calls the ptrace attach that we are using may cause idle threads to wake up momentarily, causing false positives when reading from procfs. For these reasons, we also have a heuristic fallback that marks known certain known calls in python as being idle.

You can disable this functionality by setting the --idle flag, which will include frames that py-spy considers idle.

How does GIL detection work?

We get GIL activity by looking at the threadid value pointed to by the _PyThreadState_Current symbol for Python 3.6 and earlier and by figuring out the equivalent from the _PyRuntime struct in Python 3.7 and later. These symbols might not be included in your python distribution, which will cause resolving which thread holds on to the GIL to fail. Current GIL usage is also shown in the top view as %GIL.

Passing the --gil flag will only include traces for threads that are holding on to the Global Interpreter Lock. In some cases this might be a more accurate view of how your python program is spending its time, though you should be aware that this will miss activity in extensions that release the GIL while still active.

Why am I having issues profiling /usr/bin/python on OSX?

OSX has a feature called System Integrity Protection that prevents even the root user from reading memory from any binary located in /usr/bin. Unfortunately, this includes the python interpreter that ships with OSX.

There are a couple of different ways to deal with this:

  • You can install a different Python distribution. The built-in Python will be removed in a future OSX, and you probably want to migrate away from Python 2 anyways =).
  • You can use virtualenv to run the system python in an environment where SIP doesn't apply.
  • You can disable System Integrity Protection.

How do I run py-spy in Docker?

Running py-spy inside of a docker container will also usually bring up a permissions denied error even when running as root.

This error is caused by docker restricting the process_vm_readv system call we are using. This can be overridden by setting --cap-add SYS_PTRACE when starting the docker container.

Alternatively you can edit the docker-compose yaml file

your_service:
   cap_add:
     - SYS_PTRACE

Note that you'll need to restart the docker container in order for this setting to take effect.

You can also use py-spy from the Host OS to profile a running process running inside the docker container.

How do I run py-spy in Kubernetes?

py-spy needs SYS_PTRACE to be able to read process memory. Kubernetes drops that capability by default, resulting in the error

Permission Denied: Try running again with elevated permissions by going 'sudo env "PATH=$PATH" !!'

The recommended way to deal with this is to edit the spec and add that capability. For a Deployment, this is done by adding this to Deployment.spec.template.spec.containers

securityContext:
  capabilities:
    add:
    - SYS_PTRACE

More details on this here: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-capabilities-for-a-container Note that if you modify the Deployment resource, this will remove the existing Pods and create those again.

You can also create an ephemeral container to attach to a running Pod, targeting a specific container where your application is running. Make sure to use a profile that grants the SYS_PTRACE permission, for example:

kubectl debug --profile=general \
    -n your-namespace \
    --target=app-container-name \
    pod-name \
    --image=python:3.12-slim \
    -it -- bash

How do I install py-spy on Alpine Linux?

Alpine python opts out of the manylinux wheels: pypa/pip#3969 (comment). You can override this behaviour to use pip to install py-spy on Alpine by going:

echo 'manylinux1_compatible = True' > /usr/local/lib/python3.7/site-packages/_manylinux.py

Alternatively you can download a musl binary from the GitHub releases page.

How can I avoid pausing the Python program?

By setting the --nonblocking option, py-spy won't pause the target python you are profiling from. While the performance impact of sampling from a process with py-spy is usually extremely low, setting this option will totally avoid interrupting your running python program.

With this option set, py-spy will instead read the interpreter state from the python process as it is running. Since the calls we use to read memory from are not atomic, and we have to issue multiple calls to get a stack trace this means that occasionally we get errors when sampling. This can show up as an increased error rate when sampling, or as partial stack frames being included in the output.

Does py-spy support 32-bit Windows? Integrate with PyPy? Work with USC2 versions of Python2?

Not yet =).

If there are features you'd like to see in py-spy either thumb up the appropriate issue or create a new one that describes what functionality is missing.

How to force colored output when piping to a pager?

py-spy follows the CLICOLOR specification, thus setting CLICOLOR_FORCE=1 in your environment will have py-spy print colored output even when piped to a pager.

Credits

py-spy is heavily inspired by Julia Evans excellent work on rbspy. In particular, the code to generate flamegraph and speedscope files is taken directly from rbspy, and this project uses the read-process-memory and proc-maps crates that were spun off from rbspy.

License

py-spy is released under the MIT License, see the LICENSE file for the full text.

Metadata

Release files for py-spy-asyncio 0.4.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for py-spy-asyncio 0.4.4
File Size Uploaded
py_spy_asyncio-0.4.4.tar.gz 305.2 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for py-spy-asyncio 0.4.4
File
py_spy_asyncio-0.4.4-py2.py3-none-win_amd64.whl Python 2, Python 3 none Windows x86-64 Details
py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl Python 2, Python 3 none Linux glibc 2.17+ ARMv7l Details
py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl Python 2, Python 3 none Linux glibc 2.17+ ARM64 Details
py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl Python 2, Python 3 none Linux glibc 2.5+ x86-64 Details
py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl Python 3, Python 2 none Linux glibc 2.5+ x86-32 Details
py_spy_asyncio-0.4.4-py2.py3-none-macosx_11_0_arm64.whl Python 2, Python 3 none macOS 11.0+ ARM64 Details
py_spy_asyncio-0.4.4-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl Python 3, Python 2 none macOS 11.0+ ARM64, macOS 10.12+ universal2 (ARM64, x86-64), macOS 10.12+ x86-64 Details

Total release size: 19.1 MB

Release files / py_spy_asyncio-0.4.4.tar.gz

Download URL py_spy_asyncio-0.4.4.tar.gz
Size 305.2 kB
Tags Source
SHA-256 checksum
How to use checksums
aeb4d961550662794998eb40c67ce8a48572c48f80e829681752d21ff9bd5e51
BLAKE2b-256 checksum
How to use checksums
73bc85565bcf0fd3395772ed9419ce0774056ebfe5a6e8d5a11f4d0e4c10aa54
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-win_amd64.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-win_amd64.whl
Size 2.0 MB
Tags Python 2 Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
f15fd718eb7f2b69ae5257d3bb3ad98b23d6e57bfe274a2a1b4bbb003bc83791
BLAKE2b-256 checksum
How to use checksums
6d9bcb6bffe0933faa8f01a1dcaf7e8d5016ccadbfa8d3509fb752f74c108bd6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Size 2.9 MB
Tags Linux glibc 2.17+ ARMv7l Python 2 Python 3
SHA-256 checksum
How to use checksums
0e262aa3a8bd6950a2b70b219eceed12e2c207965c6f85cc2d3260ebb9e72985
BLAKE2b-256 checksum
How to use checksums
1baf129b052fba23ad8a90ec1406766dbfc064e03cd45054ab16e953b518d5f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 2.9 MB
Tags Linux glibc 2.17+ ARM64 Python 2 Python 3
SHA-256 checksum
How to use checksums
0002da913e98929e60070d0d4398ca5c77a1b920d049de0ed7aa8a331252f88e
BLAKE2b-256 checksum
How to use checksums
c97f764d2ae870f8f27fb51a789e27eec2c5227980d19423c48ccd588f9a22e5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
Size 3.0 MB
Tags Linux glibc 2.5+ x86-64 Python 2 Python 3
SHA-256 checksum
How to use checksums
66ec38963f8902823e7a3d0be30d6f9485dd2f2b5372b378b14e083ee75ccb1e
BLAKE2b-256 checksum
How to use checksums
e8ba7af02d517eb55f081171b39c64e35c4a434a64bc9c82ac726e7d6ea09b0c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl
Size 2.4 MB
Tags Linux glibc 2.5+ x86-32 Python 2 Python 3
SHA-256 checksum
How to use checksums
3063feec2393adf428415004b5d2d159766c9443f86a7b229a624ca4bf500240
BLAKE2b-256 checksum
How to use checksums
399b6fbfceb4b869de14f7101a637a414f8f9602fe853c9550e21e454d10ef07
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-macosx_11_0_arm64.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-macosx_11_0_arm64.whl
Size 1.9 MB
Tags Python 2 Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
256bf0f2357a755fbb968bcb587308d5c7f65e26ca0d2d9f3a0d1ebf06a3dae0
BLAKE2b-256 checksum
How to use checksums
bc9e809deccd991ce0681ed7574b33ce1d8b1ac340280a5ee24aac017d1f89a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / py_spy_asyncio-0.4.4-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl

Download URL py_spy_asyncio-0.4.4-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
Size 3.8 MB
Tags Python 2 Python 3 macOS 10.12+ universal2 (ARM64, x86-64) macOS 10.12+ x86-64 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
f2113cd7524e09fdec3361a87d4a46f4e8af612d3bb7498676c90287d3dbd534
BLAKE2b-256 checksum
How to use checksums
0fcaee310ea83997c029d9da3a13b5ffd0cc2acd28285abab3ad6fc3b918819f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release history Release notifications | RSS feed

This release

0.4.4 This release

8 release files

0.4.3

8 release files

0.4.2

3 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page