Skip to main content

Sampling profiler for Python programs

Project description

py-spy: Sampling profiler for Python programs

Build Status FreeBSD Build Status

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.9).

Installation

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.

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.

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 x86_64 Linux and Windows. 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.

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 this will remove the existing pods and create those again.

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.

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

py_spy-0.3.11.tar.gz (155.6 kB view details)

Uploaded Source

Built Distributions

py_spy-0.3.11-py2.py3-none-win_amd64.whl (1.4 MB view details)

Uploaded Python 2 Python 3 Windows x86-64

py_spy-0.3.11-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl (2.7 MB view details)

Uploaded Python 2 Python 3 manylinux: glibc 2.17+ ARMv7l

py_spy-0.3.11-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.5 MB view details)

Uploaded Python 2 Python 3 manylinux: glibc 2.17+ ARM64

py_spy-0.3.11-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl (3.0 MB view details)

Uploaded Python 2 Python 3 manylinux: glibc 2.5+ x86-64

py_spy-0.3.11-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl (2.8 MB view details)

Uploaded Python 2 Python 3 manylinux: glibc 2.5+ i686

py_spy-0.3.11-py2.py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl (3.1 MB view details)

Uploaded Python 2 Python 3 macOS 10.9+ universal2 (ARM64, x86-64) macOS 10.9+ x86-64 macOS 11.0+ ARM64

py_spy-0.3.11-py2.py3-none-macosx_10_7_x86_64.whl (1.6 MB view details)

Uploaded Python 2 Python 3 macOS 10.7+ x86-64

File details

Details for the file py_spy-0.3.11.tar.gz.

File metadata

  • Download URL: py_spy-0.3.11.tar.gz
  • Upload date:
  • Size: 155.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.8

File hashes

Hashes for py_spy-0.3.11.tar.gz
Algorithm Hash digest
SHA256 3115c6e45cdab657f81c886f8b1664e7e2208b775396031eeea60e4d16fc861d
MD5 e4bfafee29a4c7e7cf1b86825e256729
BLAKE2b-256 26da6533d435c5c421cd7e186620b81db14145bbb6e18d408a6af96cc497143d

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-win_amd64.whl.

File metadata

  • Download URL: py_spy-0.3.11-py2.py3-none-win_amd64.whl
  • Upload date:
  • Size: 1.4 MB
  • Tags: Python 2, Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.8

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 f014b383a14e5d881710ab930c435cbe76b8af40855bbc09775b70a0254b600a
MD5 4c6414200835376a07bf8a2c41c5dbe2
BLAKE2b-256 52ca70a2f319a81caa95947afb88db6636c22f090dc8237cb6cc6e13bf94f056

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl.

File metadata

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl
Algorithm Hash digest
SHA256 e35a4720859dfd86238df5ac17d63d1f08901d40953ff5eac0bda5afdf744b22
MD5 c1a1993bab1c1250898f9224b20d9aff
BLAKE2b-256 d8dbcdc84b358f1ba8b8ed822e32037f29baad1ac2a457b794ae052372b4351c

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 3fbd42d211fe5c4cdf650b194940c1f053fe30b5fbba09e933432535a13867a5
MD5 c742fb40bb0b702e5f950665d736f182
BLAKE2b-256 6fad4f1edc5040eafbffcf2edb9c511a1ea8d1d940a3d32a987539269e612178

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl.

File metadata

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
Algorithm Hash digest
SHA256 6f7ba3ae9f6a61ca2e10367427ee8d39d46880b09a78a90e6899b02601187c8d
MD5 d10e2c1551d1dbc5ebb4873632f52da7
BLAKE2b-256 f32571e793126c0649bb42f847b5f68e81246df46a582f1f5a9f64373e343694

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl.

File metadata

  • Download URL: py_spy-0.3.11-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl
  • Upload date:
  • Size: 2.8 MB
  • Tags: Python 2, Python 3, manylinux: glibc 2.5+ i686
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.8

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl
Algorithm Hash digest
SHA256 3b80db1630f5ca1314b72e7f739c00329bc44a8fd06f44a2d708267c248fce2f
MD5 43546040b2c020e72d34b4577986e49a
BLAKE2b-256 cb7d6195d1ec2210367b7a629445c52c1cd02ca0cd2078e43e25b970aabb76fe

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl.

File metadata

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl
Algorithm Hash digest
SHA256 249af0490f04aea58606fa18c82ea33a25a3894e32ba019367b62a4b8d54f5a6
MD5 738b6637d9f4685f64b2d52f7a8ad5f8
BLAKE2b-256 3379fa1a2bc8df856efa7b99fef94c4ca80a9be1b4108190225a698c0be1ba5f

See more details on using hashes here.

File details

Details for the file py_spy-0.3.11-py2.py3-none-macosx_10_7_x86_64.whl.

File metadata

  • Download URL: py_spy-0.3.11-py2.py3-none-macosx_10_7_x86_64.whl
  • Upload date:
  • Size: 1.6 MB
  • Tags: Python 2, Python 3, macOS 10.7+ x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/3.6.0 importlib_metadata/4.8.2 pkginfo/1.7.1 requests/2.26.0 requests-toolbelt/0.9.1 tqdm/4.62.3 CPython/3.9.8

File hashes

Hashes for py_spy-0.3.11-py2.py3-none-macosx_10_7_x86_64.whl
Algorithm Hash digest
SHA256 5dbec592e43bb10ceff887f2cf889e2b45d1dd999363c9241637428599eeb358
MD5 853bfea6e29325208f7aeb260b5e37ef
BLAKE2b-256 2ec5da8d631895fb2f1b5e12b563ccb45de412b9fe1a5b514f897297a692c8a9

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page