Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

drain

Image

Battery-drain triage TUI. Catches the polling loop you didn't mean to write.

Version Rust License Platform Stay Amazing

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.

Why ctx-switches/s?

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.

Features

  • 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 to QueryTree on minimal WMs
  • Pinned suite summaryglass×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.json on 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 -c integration (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, wifi power_save off, or a kernel wakelock. Each costs hundreds of mW while CPU%/wakes read zero. Header shows an orphans:N badge (green 0 · yellow ≤2 · red ≥3). In the view, k/K SIGTERM/SIGKILL the holder (with confirm), a adds 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 --orphans prints a headless scan.)
  • Firefox tab view (Enter on a firefox-bin row) — lists Firefox's Fission content processes (each Isolated Web Content / Web Content is a tab or site) sorted by drain, so the rogue tab floats to the top. k / K kill the selected one (Firefox shows a recoverable "tab crashed"). For tab titles, see Firefox's about:processes.
  • claude analysis paneclaude -p runs at startup and on I with the top drainers as prompt; suggests likely polling-loop candidates. Ctrl+Y copies the analysis to your clipboard via OSC 52 (no xclip fork)
  • 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 (p or f) — freeze sampling so you can read the table

Install

cargo build --release
ln -sf $(pwd)/target/release/drain ~/bin/drain

Static-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 bindings

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

Workflow when battery W jumps from 4 → 7+

  1. Open drain. Header shows current Bat W with Δ vs the rolling average and Δ vs your persistent baseline.
  2. Press s to cycle to wakes sort. The top of the list is the suspect.
  3. If you suspect a recent change in your own code, press d to enable diff mode. Rows that have spiked above their historical baseline get an ↑↑↑ / ↑↑ / badge.
  4. Press ↑/↓ to highlight the row. Enter expands threads; S attaches strace and shows the syscall breakdown in the analysis pane on the right.
  5. The WS column tells you which glass / firefox / slack / etc. instance it is when you have several.
  6. The pinned suite line at the top shows aggregate wakes/s for the asm tools — instant "is the foundation still cold?" check.

Layout

 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

Drain score

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.

Limitations

  • /proc/<pid>/io is restricted on Linux — non-owned PIDs report 0 for I/O without CAP_SYS_PTRACE. Fine for your own user-session drain hunting; matters if you want to chase a daemon.
  • _NET_WM_PID is 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_PID but no _NET_WM_DESKTOP — common on minimal WMs that don't publish per-window desktop info.
  • strace -c needs ptrace permission. See Install notes above.
  • claude pane needs the claude CLI in $PATH. Without it, I and 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 drain itself only refreshes once per second to keep its own footprint negligible. drain appears in its own table — pressing + to slow the refresh drops drain's own footprint visibly.

Files

~/.cache/drain/state.json   persistent baseline (bat W avg + per-comm wakes/cpu medians)

Related

  • glass — terminal where drain runs
  • tile — WM that publishes the _NET_WM_DESKTOP drain 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

About

Battery-drain triage TUI for Linux. Catches the polling loop you didn't mean to write. Part of the Fe₂O₃ Rust terminal suite.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages