v0.2.0
Pre-release[0.2.0] - 2026-07-30
Two more screens, the container facts the collector was already reading, and a way to
watch one process together with everything it spawned.
Added
Two new screens
- A CPU screen at
3, which renumbers Storage to4, Network to5and Inspect to
6. It groups the per-core meters by core class —PERFORMANCEandEFFICIENCYon
Apple Silicon, and whatever else a platform names — because four efficiency cores at 90%
with four performance cores idle is a machine doing very little, and the opposite is a
machine working hard, from the same eight numbers. The load average is shown per logical
CPU beside it, since11.4means different things on 4 cores and on 128. A machine that
reports one kind of core gets one panel, which is a fact rather than a missing feature. - A Battery screen at
7. Charge, state, time remaining only where the platform
reports it — never derived from a rate — cycle count, current capacity against design
capacity with the resulting wear, pack temperature, and draw in watts. Plus a thermal
sensor panel: those readings previously reached the screen as a single figure in the
Overview header. A machine with no battery says so once, in the panel label, rather than
filling a screen with placeholders. On macOS, cycles, capacity, wear, pack temperature
and watts all readn/a: they live in the undocumentedAppleSmartBatteryregistry
properties that §9.3 forbids this build from reading. The charge, the state and the time
remaining are real; the rest is honest absence, and the Linux collector reports all of it. - The Linux battery collector, from
/sys/class/power_supply/*on the medium tier.
docs/platform-support.mdhad been promising it. The system battery is picked by
type == Batterywithscopeabsent orSystem, which is what excludes the charger and
a wireless mouse's cell; amp-hour drivers are converted throughvoltage_min_design, and
without that the capacity stays unavailable rather than being guessed.
Follow one process with its children
Fscopes the process table to the selected process and everything beneath it, and
Fagain lifts the scope. The palette hasfollow [pid]andunfollow. The panel title
saysfollowing 410while it is on, because four rows out of a thousand with no reason on
screen is indistinguishable from a monitor that has lost the other processes.- The trailing label carries what the whole family costs —
4 of 10 total, cpu >=107%, rss 479M. That figure is the point: a build's compilers come
and go every second, so no individual row ever answers "what is this build using", and a
text filter onccfinds every compiler on the machine including someone else's. >=marks a sum as a lower bound. A member whose CPU the OS refused is still counted
as a member and still shown withdeniedin its cell, but it cannot contribute to the
total. A sum with no contributors reports the members' own state, so an all-refused
family readsdeniedrather than0%.- Membership is downwards only — the process and its descendants, never its parent — and
the root is a(pid, start key)pair, so a recycled PID cannot quietly become the thing
being followed. When the root exits, monitrs stops following and says so rather than
silently following the orphans that init inherited. Losing only children changes nothing.
Container awareness on Linux
- The cgroup CPU ceiling is shown beside the host's CPU count, never instead of it:
8 logical, 8 physical, cgroup 1.5 CPUson Inspect,cgroup 1.5 cpuin the header, and
the same figure in the CPU screen's title. A group limited to 1.5 CPUs on a 64-CPU machine
is not "2% of the machine"; it is a wall its processes are throttled against. An unlimited
group is unsupported rather than a very large number of CPUs, and acpu.maxthat cannot
be parsed is unavailable rather than "no limit", because those mean opposite things to
someone trying to explain a stall. - The cgroup's own memory charge, from
memory.current— the counter the kernel compares
againstmemory.maxwhen it decides to OOM-kill — beside the limit it is enforced against:
cgroup limit 2.0G, 512M used (25%)./proc/meminfois not namespaced, so thememory
row is the host's: a process in a 2 GiB group on a 64 GiB host reads the host's 40 GiB
and concludes it is nearly out of memory when it has used 300 MiB of its allowance. Both
halves of that ratio now come from the group, or neither does. - The container is named where the cgroup path identifies one —
container docker 3f4a1b2c9d8e, orkubernetes/containerd …— on the Inspect screen's environment row,
ahead of the evidence, so a narrow panel truncates "how we guessed" rather than "what this
is". A/.dockerenvfile is evidence of a container that names no container, and that case
stays unnamed rather than being filled in with a placeholder. - The load average is deliberately not divided by the cgroup quota.
/proc/loadavgis
not namespaced either: inside a container it counts every runnable process on the machine,
including other tenants'. The divisor stays the host's CPU count and the label becomes
over 8 host cores, so the figure is visibly about the machine. Inventing a container's
saturation by division would be worse than not having it.
The Storage screen
It had two panels and thirty blank rows. It now has four.
- A
TOP DISK I/Opanel ranking processes by read and write rate, with cumulative
totals. Two ordering rules do the work: a process whose counters were refused sorts below
a measured idle one, because a refusal is not zero; and where rates tie — which on a real
machine is nearly every process — the tie-break is the cumulative totals rather than the
PID. Without the second rule the panel is twenty-eight rows of0B/sin launch order. - Inode usage per filesystem, from
getfsstaton macOS andstatfs(2)on Linux. A
filesystem with no inode table reports unsupported, never0 of 0, and a refused read
readsdeniedrather than collapsing into then/athat means "no inode table". - Mounts that share a device are marked, with the panel stating that
SIZE is not additive. An APFS Mac lists the same disk under several mount points, and adding those
figures up is a mistake the screen used to invite. - A throughput history panel from the retained ring, labelled as the machine aggregate,
because the ring keeps no per-device series and fabricating one is not allowed.
Open files and sockets
- The process detail overlay names the descriptors, not just counts them: file, socket,
pipe, event queue, shared memory, semaphore, with the path where there is one. Capped at
256 per process, with the number not listed stated rather than the list silently ending. - Socket counts on macOS flipped from unsupported to available. The old comment claimed
a syscall per descriptor;PROC_PIDLISTFDSalready carries the type of every descriptor,
so the count costs 3 µs where reading the paths costs 216 µs — measured, on a
442-descriptor process.
Pressure escalations are announced
- A Pressure Radar signal changing state now produces a notice quoting the diagnostics
engine's own rule text, not a paraphrase. Recoveries are announced too, atinfo
severity, because a signal that goes quiet without a word looks the same as one that
stopped being collected. An unavailable signal is
never a de-escalation: it clears the remembered state, so the samples either side of a
permission error cannot be stitched into a change that never happened. diagnostics.bell_on_critical(defaultfalse) rings the terminal bell once per
sample on escalation into critical. Configuring it whilediagnostics.enabled = falseis
reported as the contradiction it is.
Changed
-
TemperatureReading::high_celsiusis nowpeak_celsius— a breaking change for
library users. It was documented as the sensor's declared high threshold, but the
underlying interface reports a high-water mark on macOS, and the two are
indistinguishable from inside the collector. A bar scaled against a high-water mark sits
at 100% forever. Onlycritical_celsiusis a declared ceiling, and it is now the only
thing a thermal scale may use. -
BatterySnapshot::healthis a method rather than a field, derived from the new
capacity, so a wear percentage can no longer disagree with the two capacities printed
beside it. Also breaking for library users. -
LinuxEnrichment::cgroup_cpu_limitreturns aMetricState<CpuQuota>instead of an
Option<CpuMax>, andMeminfoSnapshot::to_snapshottakes the group's charge alongside
its limit. The fields those changes added are in the consolidated list below. -
MemorySnapshot::effective_limit_bytesnow accepts a stale cgroup reading, which is
the one place this codebase deliberately breaks its own "fresh values for calculations"
rule. A limit is configuration, not a measurement: if the last good read said 2 GiB and
this tick's failed, falling back to the host's 64 GiB advertises 62 GiB of headroom that
does not exist.CpuSnapshot::effective_coresis its new counterpart and follows the same
rule; it is new in this release rather than changed. -
The JSON export schema is version
2.monitrs snapshot --format jsonreports
"schema_version": 2, and a consumer that checks it — which is what the field is for —
will now refuse rather than misread. Two fields were removed:
sensors.battery.healthandsensors.temperatures[].high_celsius. The second's
replacement,peak_celsius, deliberately means something else, so a script reading the
old name as a declared limit would have silently reinterpreted a high-water mark as one.
The oldhealthiscapacity.full_microwatt_hours / capacity.design_microwatt_hours.
Added, which alone would not have earned a bump:cpu.cgroup_quota,cpu.core_classes,
memory.cgroup_used_bytes,filesystems[].inodes,
sensors.battery.{capacity, temperature_celsius, power_watts}, and
host.environment.available.containeron Linux. -
Six public enums gained variants, and none of them is
#[non_exhaustive], so a
downstreammatchno longer compiles until it grows an arm:
monitrs_core::process::ProcessPredicate::InSubtree,monitrs_tui::ViewId::{Cpu, Battery},
monitrs_tui::Action::{FollowSelected, StopFollowing},monitrs_tui::Effect::RingBell,
monitrs_tui::app::Command::{Follow, Unfollow}, and
monitrs_tui::app::NoticeKind::Pressure. Two public constants changed type as a
consequence:ViewId::ALLis now[ViewId; 7]andNoticeKind::ALLis[NoticeKind; 8],
so a spelled-out array length fails to compile.These types stay exhaustive on purpose.
Effectin particular is matched without a
wildcard in the one function that acts on the outside world, and that exhaustiveness is
what forces a new effect to be wired to something real instead of silently doing nothing. -
Nine public fields were added to seven public structs, breaking struct literals.
Four have noDefault, so every literal breaks:HostEnvironment::container,
FilesystemSnapshot::inodes,ProcessDetail::open_file_list, and
macos::MachineFacts::core_classes. Three have one, so only fully exhaustive literals
break:Scenario::{asymmetric_cores, process_open_files, battery_state},
linux::LinuxSources::power_supplies, andapp::AppSettings::bell_on_critical. Add to
thoseCpuSnapshot::{cgroup_quota, core_classes},MemorySnapshot::cgroup_used_bytes,
andBatterySnapshot::{capacity, temperature_celsius, power_watts}. -
The screens renumbered, so
ViewId::to_digitandViewId::from_digitanswer
differently. Inserting CPU at3moved Storage to4, Network to5and Inspect to
6;5now opens Network for anyone with the old key in their fingers or their scripts. -
Below 92 columns the tab strip shows bare digits instead of screen names —
[1] 2 3 4 5 6 7rather than[1 Overview] 2 Processes …. Seven titles need 76 cells beside the
footer hints, where five needed 58, so an 80-column terminal that used to show the names
no longer can. While the timeline is frozen the footer carriesL livetoo and the
cutover rises to 100 columns, which reaches into the Standard band. The active screen
stays bracketed, and §5.2's rule that colour is never the only carrier still holds.
Fixed
-
Storage listed one row per mount point rather than per device, so an APFS Mac showed
the same disk four times with four identical throughput figures — and a reader adding
them up got four times the machine's real I/O. One row per device now, with every mount
point it backs named in the row. -
OPEN FILESon macOS was the descriptor table size, not the count. A process
reporting 25 held 3 descriptors, opened 20 more, and still reported 25; another reporting
1600 held 453. It now comes from the enumeration. -
ROOTin the process detail rendered as an empty cell for every process on the
machine, because the kernel fillspvi_rdironly when it differs from/. It is now
/, and never the empty string. -
Sub-absolute-zero temperatures reached the screen. Every Apple Silicon Mac reports
about eight unwiredPMU tdev*sensors at roughly −9200 °C, and the new battery screen
printed them as temperatures. The collector discards them; 25 readings become 17 real ones
on the machine this was found on. -
A failed
cpu.maxread kept the previous value, presenting a ceiling read minutes
earlier as the current one. An unparsable limit is now unavailable. -
cargo docwas broken on Linux — a public module doc in the newstatfsreader
linked to a private constant — and nothing on a macOS workstation could see it, because
neithercargo check --targetnorclippy --targetruns rustdoc. The release checklist
now runs all three against the other platform. -
Three documents still sent the reader to
5for the Inspect screen after the
renumbering: the README, the troubleshooting guide and the accessibility review. -
The command palette's
viewhint and its rejection message listed only the five 0.1.0
screens, socpuandbatteryworked but were undiscoverable. The rejection now names
every screen, and a test walksViewId::ALLso the next screen cannot be added without
one.
Known limitations
Carried forward and re-checked rather than copied: what 0.1.0 listed and this release still
owes, minus what is now fixed.
- The twelve-hour soak is still not on record, and it is now explicitly out of scope
here: it will be run on a dedicated EC2 host under its own project rather than on a
workstation, because the gate §16.1 asks for is twelve uninterrupted hours and a laptop
that sleeps does not produce one. The 30-minute run with the shipped collector remains
the only evidence, and it showed no growth. Nothing has been soaked on Linux, which
is also the only configuration where the file-descriptor budget is exercised at all. - Idle self-CPU still misses its p95 budget: median 0.5–1.1% against 1% — met — and p95
6–11% against 2%.docs/benchmarks.mdlocates the cost read by read; it is OS reads
rather than monitrs' own computation. - Two of the seven screens have never been seen on Linux. The Battery screen's Linux
collector is tested from captured/sys/class/power_supplyfixtures on macOS, and the
inode reader fromstatfserrno cases, but no Linux machine has run either. - A slow terminal can still block a frame for an unbounded time, and no instrument here can
see it. - Below 92 columns the tab strip loses the screen names, which is new in this release
and is the cost of going from five screens to seven. - Device busy time remains unsupported on macOS: there is no documented API. Per-process
socket counts, which 0.1.0 listed here, are now supported — one syscall already carried
them. - PSI is Linux-only, and says
n/aon macOS rather than promising a value that will never
arrive. - Timestamps are UTC and labelled
Z: no time-zone database is bundled. - Of the six published archives, only the two macOS ones have ever been run, and the
x86_64 one only under Rosetta. That is unchanged from 0.1.0.
Verifying a download
sha256sum --check --ignore-missing SHA256SUMS
gh attestation verify monitrs-*.tar.gz --repo gaborini/monitrs