Battery-drain triage TUI. Catches the polling loop you didn't mean to write.
Small TUI that answers one question fast: what is draining my battery right now, and did I just make it worse?
top and htop show CPU%. powertop is the kernel-level truth but
needs a tracing kernel, takes seconds to settle, and floods the
screen with what you didn't ask. drain reads /proc directly,
samples once per second, and ranks processes by a single drain score
combining CPU%, voluntary context switches/s (the polling
proxy), and I/O bytes/s.
It also pairs with glass and
tile (the CHasm asm WM/terminal):
the WS column attributes each process to its workspace via
_NET_WM_PID + _NET_WM_DESKTOP, so you can tell which glass is
the rogue one when you have several. A pinned suite summary line
shows aggregate wakes/s for glass / tile / strip / bare /
asmites at a glance.
Part of the Fe₂O₃ Rust terminal suite.
A polling loop wakes the CPU once per timer tick. Each wake counts
as one voluntary context switch in /proc/<pid>/status. So
Δvoluntary_ctxt_switches / Δt is the wakes-per-second rate — the
single best signal for "I introduced a poll where I should have used
an event wait". Watching CPU% alone misses the worst kind of waste:
the tight 10ms timer that uses 0.1% CPU but kills your battery from
the wakeups it causes the whole CPU package to do.
- Per-process top-N — CPU%, wakes/s, NVOL/s (preempted/s), I/O, and a five-dot weighted drain score
- Battery W in header — current draw, Δ vs rolling 30-sample average, Δ vs persistent baseline, hours-remaining estimate
- WS column — workspace attribution via EWMH
_NET_WM_PID+_NET_WM_DESKTOP. Falls back toQueryTreeon minimal WMs - Pinned suite summary —
glass×3 21.5w/s · tile 4.8w/s · …always-visible "is the asm suite behaving?" - Diff mode (
d) — highlights processes whose wakes/s rate is significantly above their own historical baseline. Catches "did this code change introduce polling?" without you remembering the old number - Persistent baseline — auto-saved to
~/.cache/drain/state.jsonon quit, reloaded on start. Carries across sessions - Thread expansion (
Enter) — drill into/proc/<pid>/task/for a multi-threaded process. Catches the case where one Firefox thread is the offender, not the whole browser strace -cintegration (S) — attach to selected pid for 3s, show syscall histogram in the analysis pane. Answers "what is it actually doing?" without leaving drain- Orphan / held-resource detector (
O) — the resource held open but idle drain class the wakes/CPU sampler misses: a non-corked audio sink, a block-mode idle-inhibitor, wifipower_saveoff, or a kernel wakelock. Each costs hundreds of mW while CPU%/wakes read zero. Header shows anorphans:Nbadge (green 0 · yellow ≤2 · red ≥3). In the view,k/KSIGTERM/SIGKILL the holder (with confirm),aadds it to a persisted allowlist. Scanned on a slow 30 s cadence (skipped while paused) — two short subprocesses + two stat reads, no polling loop. (drain --orphansprints a headless scan.) - Firefox tab view (
Enteron afirefox-binrow) — lists Firefox's Fission content processes (eachIsolated Web Content/Web Contentis a tab or site) sorted by drain, so the rogue tab floats to the top.k/Kkill the selected one (Firefox shows a recoverable "tab crashed"). For tab titles, see Firefox'sabout:processes. - claude analysis pane —
claude -pruns at startup and onIwith the top drainers as prompt; suggests likely polling-loop candidates.Ctrl+Ycopies the analysis to your clipboard via OSC 52 (noxclipfork) - Filter (
/) — substring match on process name. Useful when you want to track one process across config changes - Configurable refresh interval (
+/-) — 0.5s to 10s - Pause (
porf) — freeze sampling so you can read the table
cargo build --release
ln -sf $(pwd)/target/release/drain ~/bin/drainStatic-linked dependencies: crust (TUI), x11rb (X11 wire
protocol for the WS column), libc (sysconf). No tracing kernel,
no root.
strace -c requires ptrace permission. If kernel.yama.ptrace_scope = 1
(the typical default), you can either run drain with sudo for
strace mode, or set kernel.yama.ptrace_scope = 0 for your session.
| Key | Action |
|---|---|
q |
quit (saves baseline) |
↑ / ↓ |
move row selection |
Enter |
expand selected pid into per-thread view |
S |
attach strace -c to selected pid for 3s |
O |
open / rescan the orphan (held-resource) view |
k / K |
in orphan/Firefox view: SIGTERM / SIGKILL the selected (y/n confirm) |
a |
in orphan view: allowlist the selected holder (persisted) |
Enter |
per-thread view — or, on a firefox-bin row, the Firefox tab view |
Esc |
close threads / strace / orphan / Firefox overlay |
s |
cycle sort: drain → cpu → wakes → io → drain |
d |
toggle diff mode (highlight anomalies vs baseline) |
/ |
enter filter; type substring; Enter apply, Esc cancel |
+ / - |
refresh interval ±0.5s (range 0.5s … 10s) |
p / f |
pause / unpause sampling |
I |
re-query claude analysis |
Ctrl+Y |
copy claude analysis to clipboard (OSC 52) |
r |
reset rolling-average bat watt readout |
h / ? |
toggle help line in footer |
- Open
drain. Header shows current Bat W with Δ vs the rolling average and Δ vs your persistent baseline. - Press
sto cycle to wakes sort. The top of the list is the suspect. - If you suspect a recent change in your own code, press
dto enable diff mode. Rows that have spiked above their historical baseline get an↑↑↑/↑↑/↑badge. - Press
↑/↓to highlight the row.Enterexpands threads;Sattaches strace and shows the syscall breakdown in the analysis pane on the right. - The WS column tells you which glass / firefox / slack / etc. instance it is when you have several.
- The pinned suite line at the top shows aggregate wakes/s for the asm tools — instant "is the foundation still cold?" check.
drain Bat 4.21 W ↓ (Δ -0.13 W vs 30-sample avg) | baseline -0.40 W 8.4h left sort: drain
suite: glass×3 21.5w/s tile 4.8w/s strip 2.4w/s bare 0.0w/s
PID PROC WS CPU% WAKE/s NVOL/s IO kB/s DRAIN
1208672 firefox 7 3.1 180.5 4.2 3.1 ●●●●●
856756 Xorg · 0.2 83.0 0.0 0.0 ●●○○○
1175734 glass 3 0.1 21.0 0.0 0.0 ●●○○○
... ─── analysis ───
Firefox at 180 wakes/s on
WS 7 dominates here. Likely
background-tab JS animation
or video. Try freezing the
tab or enabling Memory Saver.
Xorg at 83 wakes/s tracks
Firefox repaints — normal.
The asm tools are quiet; the
suite is behaving.
q · ↑↓ · Enter · S · s · d · / · +/- · p · I · C-y · r · h help drain v0.1.3
A weighted sum, tuned for a 4 W idle laptop:
drain = cpu_pct + wakes_per_s × 0.05 + (io_bytes/1024) × 0.02
Five dots map roughly to:
| Dots | Drain | Typical cause |
|---|---|---|
●●●●● |
≥ 50 | Tight CPU loop or 1000+ wakes/s |
●●●●○ |
≥ 20 | Heavy work; one busy app |
●●●○○ |
≥ 8 | Background polling that adds up |
●●○○○ |
≥ 3 | Active but reasonable |
●○○○○ |
≥ 1 | Almost-quiet |
○○○○○ |
< 1 | Idle |
Rationale: a tight 10% CPU loop is worse than 200 wakes/s of an otherwise-sleeping process, but 200 wakes/s of context-switch overhead is worse than 4% CPU of useful steady work. The 0.05 weight on wakes hits both with the right severity.
/proc/<pid>/iois restricted on Linux — non-owned PIDs report 0 for I/O withoutCAP_SYS_PTRACE. Fine for your own user-session drain hunting; matters if you want to chase a daemon._NET_WM_PIDis set by most apps but not all (some Java Swing apps, some headless tools). Those show·in the WS column.?means the window has a_NET_WM_PIDbut no_NET_WM_DESKTOP— common on minimal WMs that don't publish per-window desktop info.strace -cneeds ptrace permission. See Install notes above.- claude pane needs the
claudeCLI in$PATH. Without it,Iand the initial query show an error in the analysis pane; everything else still works. - Sub-second polling loops between samples are still visible (they
show up as elevated wakes/s) but
drainitself only refreshes once per second to keep its own footprint negligible.drainappears in its own table — pressing+to slow the refresh drops drain's own footprint visibly.
~/.cache/drain/state.json persistent baseline (bat W avg + per-comm wakes/cpu medians)
- glass — terminal where drain runs
- tile — WM that publishes the
_NET_WM_DESKTOPdrain reads - strip — status bar segment helpers; one of drain's first targets when you suspect "did my asmite refactor regress?"
- Fe₂O₃ landing page — the family