ptop is an interactive TUI for deep inspection of Linux processes via eBPF.
Live diagnosis of CPU, syscalls, network, I/O, memory, threads, and file
descriptors — without restarting, instrumenting, or changing a line of code
in the target.
| Tab | Shows | eBPF source |
|---|---|---|
| F1 Overview | CPU sparkline, top syscalls, threads, I/O, FDs, network, heap/leak, events | aggregate |
| F2 Syscalls | per-call frequency, latency, live event stream | raw_syscalls:sys_{enter,exit} |
| F3 Network | TCP/UDP/UNIX connections with state, RTT, Tx/Rx | sock:inet_sock_set_state + tcp_sendmsg/cleanup_rbuf kprobes |
| F4 Threads | per-TID state, on-CPU%, lock graph (futex) | sched:sched_switch + futex tracepoints |
| F5 I/O | dual throughput, top files, latency histogram | VFS read/write/fsync syscall tracking |
| F6 FDs | live FD table by type, with bytes and active flag | /proc/<pid>/fd + open/close events |
| F7 Timeline | unified event stream tagged by category | all of the above |
A real go test dump from internal/tui/dump_test.go. Every panel matches
what the live binary renders against a real PID.
⬡ ptop │ api-server PID 1 Go 1.25 RUNNING 15 fds uptime 00:00 │ 18:06:31
F1 Overview │ F2 Syscalls │ F3 Network │ F4 Threads │ F5 I/O │ F6 FD │ F7 Timeline q quit · / filter · p pause
┌──────────────────────────────────────────────────────────────────────────────────┐┌──────────────────────────────────────────────────────┐
│ ▸ CPU ││ ▸ I/O THROUGHPUT │
│ ⡀⡀⡀ ⡀⡀ ⡄ ⡀ ⡀⡀ ⡀⡀⡄ ⡀⡄⡀⡀⡀⡄⡄⡀⡄⡀ ⡀⡄⡀⡀⡀ ⡀⡄⡀ ⡄⡀⡄⡀⡀ ⡀⡄⡀⡀ ⡀⡀ ⡀⡀ 20%││ ⡏⡆⡏⡄⡇ ⡄ ⡟⡟⡏⡄⡟ ⡟⡟⡄⡆⡏ ⡏⡟⡇ ⡆ ⡆⡟⡇⡀⡀⡏⡇⡿⡀⡀⡀⡇ read/s │
│ cpu usage││ 494.2KB │
│ ││⡄⡇⡇⡟⡆ ⡇ ⡆⡀⡏ ⡆⡄⡇⡄⡄⡀⡄⡏⡇⡏⡏⡀⡇⡀⡀⡀⡏⡏⡿⡄⡇⡄⡆⡏⡀ ⡟ write/s│
│ ││ 333.5KB │
└──────────────────────────────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────────┐┌──────────────────────────────────────────────────────┐
│ ▸ TOP SYSCALLS ││ ▸ FILE DESCRIPTORS │
│poll ████████████████████████████████████████████████████████████ 195││file ████████████████████████████████████████ 5│
│read ███████████████████████████████████████████████████████████░ 194││socket ████████████████████████████████░░░░░░░░ 4│
│write ████████████████████████████████████████████████████████░░░░ 184││pipe ████████████████████████████████░░░░░░░░ 4│
│openat ███████████████████████████████████████████████████████░░░░░ 181│└──────────────────────────────────────────────────────┘
│fstat ██████████████████████████████████████████████░░░░░░░░░░░░░░ 151│┌──────────────────────────────────────────────────────┐
│getpid ████████████████████████████████████████████░░░░░░░░░░░░░░░░ 143││ ▸ NETWORK │
│epoll_wait ███████████████████████████████████████████░░░░░░░░░░░░░░░░░ 142││TCP → 10.0.1.5:5432 WAIT 42ms │
│recvmsg █████████████████████████████████████████░░░░░░░░░░░░░░░░░░░ 135││TCP ↔ 10.0.0.1:443 ESTABLISHED 8ms │
└──────────────────────────────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────────────────┐┌──────────────────────────────────────────────────────┐
│ ▸ THREADS ││ ▸ EVENT STREAM │
│▶ main ███████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 34% ││18:06:31.367 CPU preempted after 12ms │
│■ worker-1 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -- ⏳ mutex-A ││18:06:31.367 SYS futex WAIT mutex-A │
│▶ worker-2 ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 18% ││18:06:31.367 LCK mutex-A released │
│· gc ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -- ⏳ nanosleep ││18:06:31.367 I/O write /var/log/app/api.log 512B │
│■ http-pool ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -- ⏳ epoll_wait ││18:06:31.367 FD openat → fd=15 /tmp/tmpXXXX │
└──────────────────────────────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────┘
F1-F7 tabs · q quit · p pause · / filter · s snapshot · e export eBPF kernel 6.8 · sampling 100Hz · overhead <0.5%
A live recording (vhs script in assets/demo.tape) will
replace this section soon.
- Linux, kernel 5.8+ (BTF + ring buffer +
CAP_BPF) amd64orarm64- For full mode: root, or the binary with
cap_bpf,cap_perfmon+ep - For building from source: Go 1.25+,
clang,libbpf-dev,bpftool
Pre-built Linux binaries (amd64/arm64) are published on each tag:
curl -L https://github.com/trentas/ptop/releases/latest/download/ptop-linux-amd64.tar.gz | tar xz
sudo install ptop /usr/local/bin/Or build from source:
git clone https://github.com/trentas/ptop.git
cd ptop
make # gen + vet + test + build-ebpfsudo ./bin/ptop --pid <PID> # full mode (eBPF)
./bin/ptop --pid <PID> --no-ebpf # /proc-only, no root
sudo ./bin/ptop --pid <PID> --fps 10 # higher render rate
sudo ./bin/ptop --pid <PID> --export # save JSON snapshot on exit
# Stream events headless (no TUI) over gRPC + JSONL
sudo ./bin/ptop --pid <PID> --serve unix:///run/ptop.sock --export
# Capture TLS plaintext around libssl (OFF by default; sensitive — see below)
sudo ./bin/ptop --pid <PID> --tls-bytes 256 --serve unix:///run/ptop.sock --exportTLS payload capture (
--tls/--tls-bytes N): uprobes the target's libssl (SSL_write/SSL_read) to record plaintext before encryption / after decryption — handy for debugging your own service's encrypted traffic without a MITM proxy. It is off by default, stream/export-only (no live panel), and the payload bytes (which may contain credentials/PII) are captured only with--tls-bytes N(capped at 4096/call). Go and statically-linked targets have no libssl, so capture is unavailable there.
| Key | Action |
|---|---|
F1–F7 (or 1–7, Tab/Shift+Tab) |
switch tab |
p, Space |
pause / resume |
/ |
filter (cycles types in F6) |
? |
help overlay (collector status with eBPF/proc/mock source) |
s |
one-shot JSON snapshot |
e |
toggle continuous JSONL export |
q, Ctrl+C |
quit |
The recommended setup is to grant capabilities once and run unprivileged:
sudo setcap cap_bpf,cap_perfmon+ep ./bin/ptop
./bin/ptop --pid <PID>If something is wrong (kernel too old, unprivileged_bpf_disabled=1, missing
caps), ptop prints an actionable error before opening the TUI:
$ ./bin/ptop --pid 1234
error: eBPF not available
Kernel 5.4 detected — ptop requires Linux 5.8+ (BTF + CAP_BPF).
On older kernels, use --no-ebpf (/proc-only mode).
Each subsystem is fed by one of three sources, picked at startup. The ?
overlay shows which one is active per tab.
| Tab | --no-ebpf (Linux) |
full mode (eBPF) |
|---|---|---|
| F1 Overview | ✅ CPU + Mem + Threads + I/O + FDs | ✅ refined by tracepoints |
| F2 Syscalls | ❌ no /proc source |
✅ raw_syscalls tracepoint |
| F3 Network | ✅ via /proc/net/{tcp,udp,unix} |
✅ + per-conn RTT/bytes |
| F4 Threads | ✅ state + CPU% + wchan | ✅ + futex lock graph |
| F5 I/O | ✅ throughput + iowait | ✅ + top files + histogram |
| F6 FDs | ✅ resolved sockets, bytes, active | ✅ same + active socket detection |
| F7 Timeline | partial — only fd events |
✅ all categories |
/proc sources used in --no-ebpf:
cpu_proc.go—/proc/<pid>/statfields 14-15 (utime+stime)threads_proc.go—/proc/<pid>/task/*/stat+wchanmem_proc.go—/proc/<pid>/statm+ page faultsiowait_proc.go—/proc/<pid>/statfield 42 (delayacct_blkio_ticks)io_proc.go—/proc/<pid>/iofds.go+sockets.go—/proc/<pid>/fd,/proc/net/{tcp,tcp6,udp,udp6,unix}proccontext_linux.go—/proc/<pid>/{status,cgroup,ns/*}→ namespace + cgroup + uid/gid (the execution/container context, #60). When the target runs in a container the derived id (docker:…,containerd:…,kubepods:…, …) shows in the header; the full context rides the--serve/--exportstream (a periodicProcContext, plus uid/gid/cgroup_id stamped on every event).
eBPF programs in internal/bpf/programs/:
syscalls.bpf.c— raw_syscalls/sys_{enter,exit}cpu.bpf.c— perf_event @ 100Hz/CPUio.bpf.c— VFS read/write/fsync + filesystem semantics (denials/deletes/renames)network.bpf.c— sock tracepoints + tcp kprobes + connection errors (RST/retransmit)threads.bpf.c— sched_switchfutex.bpf.c— wait/wake → lock graphmemory.bpf.c— mmap/brk/page-fault countersheap.bpf.c— libc malloc/free uprobes → live-heap + lifetime + leak suspectssignal.bpf.c—signal:signal_generate→ signals delivered, with senderproc.bpf.c—sched_process_{fork,exec,exit}→ exec-lineage subtreesecurity.bpf.c— PROT_EXECmmap/mprotect+ SELinux AVC denialstls.bpf.c— libsslSSL_write/SSL_readuprobes → plaintext (opt-in--tls)
The TUI is one consumer of a richer event model. ptop --pid <PID> --serve <addr> runs headless and streams every observation as a typed protobuf Event
over gRPC (package ptop.v1) to any number of unprivileged subscribers — and,
with --export, also as one protojson line per event to a JSONL file. ptop
holds CAP_BPF/CAP_PERFMON; subscribers connect with none (the unix socket is
0600, TCP refuses non-loopback binds).
Beyond the seven TUI tabs, the stream carries the full process-behavior surface
(each event tagged by category, with uid/gid/cgroup_id stamped on every
envelope):
| Category | Event | What it captures |
|---|---|---|
MEMORY |
HeapEvent / HeapSnapshot |
libc malloc/free paired → live-heap, lifetime, leak suspects, top call sites (symbolized) |
NETWORK |
NetErrorEvent |
TCP failures: connection refused, reset, retransmits |
NETWORK |
TLSPayloadEvent |
pre-encryption / post-decryption plaintext (opt-in --tls) |
IO |
FSEvent |
filesystem semantics: permission denials, deletes, renames (real paths) |
SIGNAL |
SignalEvent |
signals delivered to the target, with the sending process |
PROCESS |
ProcContext |
namespace + cgroup + uid/gid (container/execution context) |
PROCESS |
ProcLifecycleEvent |
exec lineage: fork/exec/exit across the descendant subtree |
SECURITY |
SecurityEvent |
runtime PROT_EXEC mappings (JIT/RWX), SELinux LSM denials |
High-rate events reference a captured stack by id; the ResolveStack RPC
symbolizes it on demand (addr → func (file:line), build-id keyed).
{"tsUnixNano":"…","pid":4242,"category":"CATEGORY_PROCESS","uid":1000,"gid":1000,"cgroupId":"2817","procContext":{"pidNs":"4026532630","cgroup":"/docker/3127f7e31dab…","container":"docker:3127f7e31dab"}}
{"tsUnixNano":"…","pid":4242,"category":"CATEGORY_PROCESS","procLifecycle":{"kind":"exec","pid":4310,"comm":"sh","filename":"/usr/bin/sh"}}
{"tsUnixNano":"…","pid":4242,"category":"CATEGORY_NETWORK","netError":{"kind":"refused","remote":"10.0.0.9:5432"}}
{"tsUnixNano":"…","pid":4242,"category":"CATEGORY_SIGNAL","tid":4242,"signal":{"signal":"SIGPIPE","signo":13,"code":128,"targetTid":4242}}
{"tsUnixNano":"…","pid":4242,"category":"CATEGORY_SECURITY","security":{"kind":"exec-map","op":"mprotect","prot":5,"callSite":{"func":"jit_emit"}}}The schema lives in proto/event.proto; collectors and
their source-priority selection are shared verbatim between the TUI and the
server, so both see identical data.
ptop/
├── cmd/ptop/ entrypoint
├── proto/ event-stream schema (package ptop.v1)
├── internal/
│ ├── bpf/ eBPF programs + loader (build tag `ebpf`)
│ ├── serve/ headless gRPC server (ptop --serve)
│ └── tui/ Bubbletea + Lipgloss views
├── pkg/ importable API surface
│ ├── collector/ /proc + eBPF collectors + shared types
│ ├── streampb/ generated gRPC / protobuf bindings
│ └── symbol/ ELF → symbol resolution (addr → func/file:line)
└── assets/ visual references + vhs script
See CLAUDE.md for the full implementation guide, type
contracts, and conventions.
make # gen + vet + test (both lanes) + build-ebpf — default goal
make test # go test -race ./...
make vet # vet in both modes (default + tags=ebpf)
make clean # rm -rf bin/ + *.bpf.o
make lint # golangci-lint (must be installed)CI runs both lanes (-tags=ebpf and default) on ubuntu-latest. See
CONTRIBUTING.md for the PR flow and commit conventions.
MIT. See LICENSE.