sgrud
Attach to a running CPython process and watch its memory, CPU, threads, asyncio tasks, stacks and garbage collector without slowing it down.
sgrud never stops or instruments the target. It reads the interpreter's
state straight out of process memory through CPython 3.15's
_remote_debugging module (the same machinery behind the Tachyon sampling
profiler and python -m asyncio ps) and pairs it with /proc for memory
and CPU accounting. A full snapshot of stacks for every thread costs tens
of microseconds on the sgrud side and nothing on the target side.
Requires Linux and CPython 3.15 or newer. The target must run the same major.minor version as sgrud itself.
Usage
sgrud PID one text snapshot
sgrud PID -n 0.5 keep printing every 0.5 s until the target exits
sgrud PID --json one JSON object per line, easy to pipe elsewhere
sgrud run -- python app.py start the target as a child and inspect it
sgrud tui PID interactive terminal interface
sgrud tui run -- python app.py
--no-stacks, --no-tasks and --no-gc drop sections you do not need.
sgrud PID --profile 5 sample stacks for 5 s, print the hottest functions
sgrud PID --profile 5 --mode gil count only the thread holding the GIL
Keys inside the TUI: 1-5 switch tabs, space pauses, r refreshes,
+ and - change the refresh interval, q quits. On the Hotspots tab
s toggles self/total ordering, m toggles wall/gil mode and c clears
the samples. The tab is fed by a background sampler (--rate, default
100 Hz) that keeps running while you look at the other tabs.
In wall mode every thread with a Python stack counts, so a sleeping thread weighs as much as a busy one. In gil mode only the GIL holder counts, which answers "where does the CPU time go" for CPython code.
Library
The TUI is only a front end. Everything comes from Monitor, which
returns plain frozen dataclasses:
from sgrud import Monitor
with Monitor.attach(pid) as m: # or Monitor.spawn(["python", "app.py"])
snap = m.snapshot() # snapshot(stacks=..., tasks=..., gc=...)
print(snap.process.memory.rss, snap.process.cpu_percent)
for t in snap.threads:
print(t.tid, t.name, t.status.describe(), t.cpu_percent, t.frames[:1])
for task in snap.tasks:
print(task.name, task.parent_ids, [f.funcname for f in task.frames])
print(snap.gc[0].collections, snap.gc[0].history[:1])
print(snap.to_dict()) # JSON friendly
Monitor.stream(interval) yields snapshots until the target exits, at which
point it raises ProcessExited. CPU percentages need two snapshots, so the
first one reports None.
For profiling, Sampler runs Monitor.sample_stacks() in a background
thread and feeds a Hotspots aggregator:
from sgrud.sampler import Sampler
with Sampler(monitor, rate=500, mode="gil") as sampler:
time.sleep(5)
for row in sampler.hotspots.rows(sort="self", limit=10):
print(row.self_percent, row.funcname, row.filename)
Permissions
Memory, CPU, thread names and per-thread CPU come from /proc and work
for any process you own. Stacks, GIL state, asyncio tasks, GC statistics
and hotspots need to read the target's memory, which needs ptrace rights.
With the default kernel.yama.ptrace_scope=1 that is only granted for
child processes.
Without those rights sgrud still attaches in limited mode and shows what it
can, with a banner explaining what is missing. To get everything, either
start the target through sgrud run -- ..., run sgrud with sudo, grant
CAP_SYS_PTRACE, or relax Yama for the session:
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
Pass require_full=True to Monitor.attach to fail instead of degrading.
A target started with -X disable-remote-debug can still be inspected.
That flag only disables code injection, which sgrud does not use.
Development
uv sync
uv run pytest
The tests spawn tests/target_app.py and inspect it, so they exercise the
real attach path. The Textual app is tested headlessly through its pilot.
Metadata
Release files for sgrud 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sgrud-0.1.0.tar.gz | 44.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sgrud-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.5 kB
Release files / sgrud-0.1.0.tar.gz
| Download URL | sgrud-0.1.0.tar.gz |
|---|---|
| Size | 44.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bad757dd098cf7cb06ab5414dfeb653f83a32bade8b2f25c570b633ca543fad3
|
|
BLAKE2b-256 checksum How to use checksums |
f7cd5c3d040f6bc4084b942b4b47ea64ff02bd536ce62f872dc3822ac2f060b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / sgrud-0.1.0-py3-none-any.whl
| Download URL | sgrud-0.1.0-py3-none-any.whl |
|---|---|
| Size | 30.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a369c60efc0c4bea8b6bd1f219f71e8b9cda6e07d217f2bffdfbc3a34519b710
|
|
BLAKE2b-256 checksum How to use checksums |
b7488c2f58c0df3061c397da68f8527f0e85ca9f2448c834b9c18774ad65bb50
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":null,"id":"forky","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|