vitals/README.md
Christian Sass b66c6619d8 docs: synthetic demo mode and a screenshot with no customer data
The first screenshot was captured on a real workstation: it carried a
machine name, a live call with a named participant, and a staging URL.
None of that belongs in a public repo.

--demo now loads demo/demo-snapshot.json and serves it through the same
API, so documentation screenshots are reproducible, reviewable, and free
of anyone's actual desktop. The canned snapshot is shaped to tell the
story: a leaked automation run at 6.10 cores with idle pages, and a video
call tab carrying 221% page work beside its idle siblings.
2026-10-19 16:49:07 -05:00

155 lines
6.1 KiB
Markdown

# Vitals
**A workload map for a browser-shaped computer.**
Every process monitor tells you which PID is hot. Nobody thinks in PIDs. Vitals
groups processes under the thing a human named — an app, a container, an agent,
a browser instance — and then crosses the boundary every other monitor stops at:
the browser. Tabs get their own numbers, so you can tell the Excel sheet that is
crashing your machine from the video call.
![Vitals](docs/screenshot.png)
## The problem
On a normal desktop, one browser is 40 to 60 processes. GNOME System Monitor
shows them as 60 identical rows named `brave`, with no age, no parent, and no
idea which one is the tab that is killing you. When ten headless browser
instances leaked from an automation run and sat on a staging page for seven days,
the monitor could not say any of this:
- that they were orphans (their launcher was long dead)
- that they were seven days old
- that their pages were idle while their GPU processes burned six cores
- which instance was which
Vitals answers all four, in one screen, without root.
## What it actually measures
Three layers, joined:
**1. Identity.** Every process is resolved to a human name using the strongest
signal available, in order: docker container, flatpak app id, systemd unit, the
top of its own process tree, or the script it runs. Kernel threads collapse to
one row. `labels.json` is where you teach it your own names.
**2. Tabs.** Chrome-family browsers that speak the Chrome DevTools Protocol are
attached per tab. `Performance.getMetrics` gives main-thread task time, script
time, heap and DOM nodes for each tab. `SystemInfo.getProcessInfo` gives
per-process CPU where the ids are real PIDs. Joining those two is what makes the
verdict possible.
**3. Verdict.** A sentence, in English:
> pages are idle (0.02 cores of page work) — 6.93 cores burned by browser
> internals (compositor / raster / media), not by a tab
That is the diagnosis no process list can produce, because the offender is a
child process of a browser that owns an idle page.
## Units, and why your numbers never matched
System monitors disagree about denominators. GNOME System Monitor reports
**percent of the whole machine**. `top` and `ps` report **percent of one core**.
The same process is `19%` in one and `225%` in the other. Vitals shows both, on
the same row, always labeled:
- `% machine` — share of all cores, matches your desktop monitor
- `cores` — the fractional core count, matches `top`
## Quickstart
Requirements: Linux, Python 3.9+, a `/proc` filesystem. No dependencies, no
root, no agent installed inside your apps.
Want to see it without pointing it at your machine? `python3 server.py --demo`
serves a canned snapshot at the same URL, which is how the screenshot above was
made.
```bash
git clone https://forge.binaryranch.com/sastian/vitals.git
cd vitals
python3 server.py # http://127.0.0.1:8791
```
Optional: run it as a user service so it is either up or reported, never a
silently orphaned process.
```bash
./install.sh # systemd --user unit, enabled at login
```
Options: `--port N`, `--host ADDR`, `VITALS_PORT` env var.
## Getting browser tabs
Tabs come from the DevTools Protocol, which a browser only exposes if it was
started with a debugging port. Chromium instances launched by automation usually
already have one, and Vitals scans ports 9500-9599 automatically. For your own
browser, add the flag once:
```bash
flatpak kill com.brave.Browser && flatpak run com.brave.Browser --remote-debugging-port=9222
```
or for Chrome/Chromium:
```bash
google-chrome --remote-debugging-port=9222
```
Tabs restore from session. Without the flag, Vitals still shows the browser's
process breakdown, age, memory and per-role CPU. It just cannot name the tabs,
and it tells you so instead of guessing.
**Security note, stated plainly:** a local debugging port lets any local process
drive that browser, including reading cookies for sites it is logged into. It
binds to `127.0.0.1` only, which keeps it off the network, but it is a real
tradeoff on a multi-user machine. Use a dedicated profile if that matters, and
close the browser when you do not need tab-level detail.
## Reading the screen
- **Machine bar** — every core on the box, split by workload. Hover a legend
entry to find its row.
- **Workload rows** — name, roles that are consuming CPU, `% machine`, `cores`,
RAM, age, 5-minute trend. Click any row to open its processes and history.
- **Flags** — `leaked` means reparented to PID 1 and older than an hour, which
is the signature of an automation run that died without cleaning up. `on a
call` means a WebRTC renderer is actually burning, not merely present.
- **Browser tabs** — every tab from every DevTools-speaking browser, sorted by
page work, with the owning browser instance, its port, its age, and how many
cores that whole instance is using. The gap between those two numbers is the
whole point.
## What it deliberately does not do
- **No root, no eBPF, no agents.** It reads `/proc`, `cgroup`, `flatpak ps`,
`docker ps`, `ss` and `wmctrl`. Everything else is arithmetic.
- **No killing.** Vitals reports. Reaping is a separate, explicit action.
- **No history beyond five minutes in memory.** Nothing is written to disk.
- **No guessing about tabs.** If a browser has no debugging port, its tabs are
unknown, and the UI says so.
## Limitations
`/proc/<pid>/fd` and `/proc/<pid>/io` require ptrace permission, so socket and
file attribution is limited to what `ss` reports. Per-tab GPU cost is inferred
from the instance's process breakdown rather than attributed per tab, because
Chromium does not expose that mapping over CDP. Kernel-thread grouping is
coarse by design.
## Roadmap
- reaping leaked browser instances from the UI, with the age and orphan
evidence shown before the button
- persisted history, so a spike at 03:00 is still there at 09:00
- per-tab process attribution via `Target.getTargetInfo` where the platform
allows it
- a second adapter for non-Chromium browsers through their own remote protocols
- macOS and BSD sampling backends, same model
## License
MIT. See [LICENSE](LICENSE).