Skip to content

trentas/ptop

Repository files navigation

ptop — process top

CI Release Go Reference Go Report Card License: MIT

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

Snapshot

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.

Requirements

  • Linux, kernel 5.8+ (BTF + ring buffer + CAP_BPF)
  • amd64 or arm64
  • For full mode: root, or the binary with cap_bpf,cap_perfmon+ep
  • For building from source: Go 1.25+, clang, libbpf-dev, bpftool

Install

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-ebpf

Run

sudo ./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 --export

TLS 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.

Keys

Key Action
F1F7 (or 17, 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

Permissions

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

Collector sources

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>/stat fields 14-15 (utime+stime)
  • threads_proc.go/proc/<pid>/task/*/stat + wchan
  • mem_proc.go/proc/<pid>/statm + page faults
  • iowait_proc.go/proc/<pid>/stat field 42 (delayacct_blkio_ticks)
  • io_proc.go/proc/<pid>/io
  • fds.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/--export stream (a periodic ProcContext, 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/CPU
  • io.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_switch
  • futex.bpf.c — wait/wake → lock graph
  • memory.bpf.c — mmap/brk/page-fault counters
  • heap.bpf.c — libc malloc/free uprobes → live-heap + lifetime + leak suspects
  • signal.bpf.csignal:signal_generate → signals delivered, with sender
  • proc.bpf.csched_process_{fork,exec,exit} → exec-lineage subtree
  • security.bpf.c — PROT_EXEC mmap/mprotect + SELinux AVC denials
  • tls.bpf.c — libssl SSL_write/SSL_read uprobes → plaintext (opt-in --tls)

Event stream (--serve)

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.

Architecture

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.

Development

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.

License

MIT. See LICENSE.

About

Interactive TUI that uses eBPF to live-inspect any Linux process — CPU, syscalls, network, I/O, memory, threads, and file descriptors.

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors