Feat/task manager - #24
Merged
Merged
Conversation
Groundwork for the Task Manager window: SensorView monitors hardware but has
had no notion of processes at all.
sensorview ps # top by CPU
sensorview ps --sort mem -n 10
sensorview ps --filter chrome --json | jq
sensorview kill <pid> [--force]
`src/procs` is a separate lane from the sensor poller, publishing an immutable
snapshot through an ArcSwap exactly as `inventory` does. Two reasons it is not
on the poll thread: enumerating ~700 processes costs tens of milliseconds, and
unlike sensors nobody needs this running when they are not looking at it — so
the collector starts and stops with its consumer rather than living for the
process lifetime.
Backed by the `sysinfo` crate rather than hand-rolled FFI. That is a deliberate
exception to this codebase's idiom: doing it by hand means three separate piles
of unsafe (libproc+sysctl, /proc, NtQuerySystemInformation) for data that isn't
what the project is about.
Two things that needed checking rather than assuming:
- **MSRV was silently pinning the dependency.** `cargo add sysinfo` resolved
0.38.4, not 0.39.6, because cargo honours `rust-version` when picking
versions and ours said 1.92 while sysinfo 0.39 wants 1.95 — precisely the
failure the existing comment in Cargo.toml warns about. Bumped to 1.95; the
toolchain here is 1.97 and CI builds on stable.
- **`user` is a separate feature from `system`.** `default-features = false,
features = ["system", "user"]` — omitting `user` would have compiled fine and
silently produced an empty owner column, the same shape as the `ureq` with no
TLS backend fixed last week. A probe test asserts both work before anything
was built on top: 734 processes, 119 users.
`cpu_pct` is `Option` and stays `None` until the collector has two samples to
difference — the same baseline problem as every rate-derived sensor here. The
CLI waits for `seq >= 2` before printing, sorts unknown CPU *last* rather than
as zero, and renders it as an em dash so a script cannot mistake "unknown" for
"idle". The footer explains that 800% on an 8-core box is per-core-summed and
not a bug.
`kill` reports failure instead of appearing to succeed — verified against pid 1:
"not permitted to signal pid 1 (owned by another user?)", exit 1.
cargo audit and cargo deny were run against the new subtree in the same pass: 4
new crates, no advisories, licences and sources clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A "Task Manager" button on the main toolbar opens a two-tab window laid out
after Mission Center: a left sidebar of categories, each with a sparkline and
its current value, and the selected one filling the right pane with a large
graph and a stats grid.
Written from scratch against SensorView's own data and palette. Nothing is
ported from Mission Center — it is GTK4/libadwaita, so there was no code that
could have been reused here even setting its licence aside.
**Performance** reuses telemetry that already exists: `s.frame()` for current
values and `store.history()` (600-sample ring) for both the sparklines and the
graph. Categories are titled by *role* — CPU, GPU, Memory, Disk 0 — matching
Mission Center's sidebar and, more practically, fixing a real collision: Apple
Silicon names its CPU and GPU nodes identically ("Apple M5"), so device-named
rows produced two identical entries. The device name moves to the subtitle, and
multiple disks/NICs are numbered.
**Processes** is a virtualised TableBuilder (`body.rows`, so ~750 processes
cost only what is on screen) with sortable headers, a name/command/pid filter,
and kill via the context menu behind a confirmation — killing is irreversible,
and it fails for processes you do not own, so the outcome is reported either
way rather than the button appearing to do nothing.
Two bugs found by looking at it rather than reasoning about it:
- The list opened **ascending**, showing the idlest processes first, because
`#[derive(Default)]` gives `descending: false`. A hand-written `Default` now
makes busiest-first explicit, with a test, since that is the entire point of
opening a task manager.
- Both CPU and GPU sidebar rows were titled "Apple M5" (see above).
The collector starts on the first frame this window draws and stops when it
closes — `Shared::procs` being `Some` *is* the running state, so there is no
second flag to fall out of sync. `ensure_collector` is called every frame and
is idempotent; `open_close_reopen_restarts_collection` covers the cycle a user
actually performs, including that a reopened collector re-establishes its own
CPU baseline rather than showing a permanently blank column.
`SENSORVIEW_SHOW_TASKMGR` and `SENSORVIEW_TASKMGR_TAB` follow the existing
dev-hook convention so both tabs can be smoke-tested without driving a mouse —
which is how the two bugs above were caught.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is where Mission Center parity actually lives. Linux was the platform
SensorView collected least on — total CPU from the first line of /proc/stat,
memory, and hwmon temps/fans, and nothing else. macOS had far more. Four
collectors close that, and they improve SensorView generally rather than only
feeding the new window:
per-core CPU /proc/stat `cpuN` lines — read_cpu_load deliberately reads
only the aggregate `cpu ` line, so per-thread did not exist
disk /proc/diskstats, sectors x 512, differenced per poll
network /proc/net/dev — HardwareType::Network, which the UI has
always been able to render but nothing ever produced on Linux
GPU /sys/class/drm/cardN/device/gpu_busy_percent (amdgpu)
Every one is a rate, so the first poll establishes a baseline and publishes
nothing — the same discipline as the macOS backend, and the reason
sensor_set_is_stable_across_polls exists.
Parsing is split from I/O (`parse_core_ticks`, `parse_diskstats`,
`parse_net_dev` take `&str`) so the tests run against captured kernel output on
any platform rather than needing a real /proc. They cover the cases that are
easy to get wrong and invisible until someone reads the numbers:
- the aggregate `cpu ` line and the `intr`/`ctxt` sections are not cores
- partitions are excluded: sda1 duplicates sda, nvme0n1p1 duplicates nvme0n1,
and counting both would double every disk
- loopback is not a network interface anyone monitors, and /proc/net/dev's two
header lines are not data
- a changed core count re-baselines instead of pairing the wrong cores
- a counter reset floors at zero rather than wrapping into an enormous rate
GPU coverage is amdgpu-only and stated as such: Intel and nouveau expose no
equivalent, and NVIDIA needs NVML. Those are gaps, not bugs.
Cross-checked with `cargo clippy --target x86_64-unknown-linux-gnu -D warnings`.
That has to skip `push`, because rustls pulls in `ring`, which needs a C
toolchain for the target — the same limitation already recorded in TODO.md, now
hit from the Linux side too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two bugs that only appeared once the collector ran on real Linux — both invisible on macOS, which is exactly why the build server was worth setting up. **Every owner was blank.** `ProcessRefreshKind`'s `UpdateKind` fields default to `Never`, so plain `refresh_processes` never populated user IDs. macOS hid this because the uid comes free with the kernel struct there; on Linux the USER column was an em dash for every row, including the user's own processes. Now spelled out via `refresh_processes_specifics`: cpu/memory/disk every tick because that is the point, user and cmd as `OnlyIfNotSet` because a process's identity does not change. Verified on the build server: those rows now read `boinc`, which is genuinely who owns them. **"4 cores" on an 8-thread box.** `System::physical_core_count()` returns physical cores, but `cpu_usage()` is per-thread — so a process on a Ryzen 5 3500U can legitimately report 800 % while the footer claimed 400 % was the ceiling. Uses `available_parallelism()` now, and says "logical CPUs". Cross-checked against `ps aux --sort=-pcpu`: same PIDs in the same order, and the count matches `nproc`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The support matrix was written before this branch and understated Linux in three places: per-core CPU, disk throughput and network were all missing rows, and storage was listed as "temperature only". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017aZ1oX66Qjq2jFQ1FeaKta
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.