Skip to content

Feat/task manager - #24

Merged
manupawickramasinghe merged 5 commits into
masterfrom
feat/task-manager
Jul 31, 2026
Merged

Feat/task manager#24
manupawickramasinghe merged 5 commits into
masterfrom
feat/task-manager

Conversation

@manupawickramasinghe

Copy link
Copy Markdown
Member

No description provided.

Manupa Wickramasinghe and others added 5 commits July 31, 2026 01:34
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
Copilot AI review requested due to automatic review settings July 31, 2026 08:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@manupawickramasinghe
manupawickramasinghe merged commit 8ccbf8a into master Jul 31, 2026
4 of 5 checks passed
@manupawickramasinghe
manupawickramasinghe deleted the feat/task-manager branch July 31, 2026 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants