docs java javascript python c / c++ c# reading reports

How to profile Python with py-spy

py-spy is a sampling profiler that reads a running Python process from the outside: no code changes, no imports, negligible overhead, safe against production. It's the right default for “why is this service slow” — cProfile answers a different question (deterministic per-call counts) at a much higher cost.

Install and capture

pip install py-spy

# profile a command end-to-end, exporting speedscope format
py-spy record --format speedscope -o profile.speedscope -- python app.py

# or attach to something already running (the production move)
py-spy record --format speedscope -o profile.speedscope --pid <pid> --duration 60

Useful flags: --idle includes threads waiting on I/O (turn it on when the app is slow but CPU is low — the answer is usually blocking calls), --native samples into C extensions, and --subprocesses follows worker processes (gunicorn, multiprocessing). On Linux, attaching to a running pid may need sudo (or --cap-add SYS_PTRACE on the container); launching the command yourself does not.

py-spy top --pid <pid> is the zero-commitment first look — a live, htop-style view of where time is going right now.

In JetBrains PyCharm

PyCharm Professional's Run → Profile button runs cProfile and opens a .pstat snapshot — good for interactive exploration inside the IDE. For a portable sampling profile (and the file flamelens will take), drive py-spy from PyCharm's terminal:

  1. Open the integrated terminal (Alt+F12) — it activates your project's virtualenv automatically — and pip install py-spy once.
  2. Either launch under py-spy right there:
    py-spy record --format speedscope -o profile.speedscope -- python main.py
  3. …or run your app normally with the Run button, then attach: find the pid in the Run console's first line (or py-spy top lists candidates) and
    py-spy record --format speedscope -o profile.speedscope --pid <pid> --duration 60
  4. The profile.speedscope file lands in your project root.

Tip: add py-spy as an External Tool (Settings → Tools → External Tools) if you do this often — arguments record --format speedscope -o profile.speedscope --pid $Prompt$.

In VS Code

  1. Open the integrated terminal (Ctrl+`) with your virtualenv selected (the Python extension activates it), and pip install py-spy.
  2. Launch under py-spy, or attach to a debugged/running process by pid exactly as above — the commands are identical:
    py-spy record --format speedscope -o profile.speedscope -- python app.py
  3. For repeatability, wire it as a task in .vscode/tasks.json:
    {
      "version": "2.0.0",
      "tasks": [{
        "label": "profile with py-spy",
        "type": "shell",
        "command": "py-spy record --format speedscope -o profile.speedscope -- python app.py"
      }]
    }

Now analyze it. Take profile.speedscope to the flamelens terminalauth <email>verify <code>scan init. A free tuning session (five scans) every month. Running a JVM service too? JFR works too.

Why there's no “scan live” for Python

For Java we can attach to a running service over JMX, and for Node over the inspector port, because both runtimes ship a remote control protocol. CPython doesn't. py-spy works by reading another process's memory directly through process_vm_readv / vm_read / ReadProcessMemory and walking the interpreter's stack structures itself. That is a local operation by construction — there is no port to connect to and nothing to expose, even if you wanted to.

Typing scan live against a Python service will tell you exactly this rather than pretend. The two real options are a py-spy file you upload, or an agent that runs py-spy for you on a schedule.

Continuous sampling with an agent ENTERPRISE

The agent runs on the same host as your service — which is what py-spy needs anyway — and connects out to us over HTTPS. Nothing inbound, no credentials held by flamelens, and a profile every N minutes diffed against the last one:

agent new prod-worker-1
# → prints a token, ONCE. store it like a password.

schedule new worker 1234 python 30 60
# → sample pid 1234 for 60s, every 30 minutes

For Python the schedule's target is a pid (or anything your agent script can resolve to one — a pidfile path, a container name, a systemd unit) rather than a host:port, since that's what py-spy takes. The agent's recorder step is one line:

py-spy record --format speedscope -o "$out" --pid "$TARGET" --duration "$secs"

Two operational notes worth getting right before you deploy it. First, ptrace permissions: attaching to a process you don't own needs sudo, or CAP_SYS_PTRACE on the agent — in Kubernetes that's securityContext.capabilities.add: ["SYS_PTRACE"], and a sidecar additionally needs shareProcessNamespace: true to see the app container's pids at all. Second, pids change on redeploy, so resolve the target each cycle (pgrep -f gunicorn, a pidfile, or py-spy dump to confirm you found a Python process) instead of pinning the pid you found once.

The full polling loop — including the JAVA/NODE/PYTHON branch — is on the JavaScript page; only the recorder line differs per runtime. Samples land in your current tuning session like any other scan.

What makes a good profile