Skip to content

0.3.1: host monitor — quantized typed HostStats, cheaper macOS/Linux process sampling (LAB-338) - #6

Merged
mbreiser merged 2 commits into
mainfrom
feat/host-monitor-quantize
Oct 29, 2026
Merged

mbreiser merged 2 commits into
mainfrom
feat/host-monitor-quantize

Conversation

@mbreiser

@mbreiser mbreiser commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two host-monitor fixes for the next package set, rig-nodes 0.3.1 (LAB-338). Both were found validating 0.3.0 in the webDisplayTools acceptance harness: typed on ww18, and the psutil path on the Mac.

1. Typed HostStats floats are quantized to power-of-two steps (host_sampler.quantize, applied by host_monitor.to_msg).

  • Problem: float32 carries ~24 bits of sampling noise per value, which zstd cannot compress. Typed /rig/host was 6.7 % of a ww18 harness run's compressed bag (1.77 MB/h), against 4.8 % for the String JSON form. The harness gate is 5 %.
  • Steps: 0..1 fractions and CPU cores to 1/1024 (0.1 %); everything else measured (rates, MB, MB/s, ms, GB, °C, W) to 1/64. Values n·2^-k are exact in float32.
  • Unchanged: NaN (absent) stays NaN, ints and strings are untouched, the caller's sample is not mutated, and the JSON path is unchanged.
  • Result on ww18: typed /rig/host 4.0–4.1 % of the compressed payload (1.02–1.05 MB/h).

2. Portable path (macOS/Linux, psutil): the whole process table every 5 s, the watched pids in between (HostSampler._ps_rows, proc_rescan_s, CLI --proc-rescan).

  • Problem: every window read cpu_percent + memory_info for every process, so the monitor cost 6.7 % of a core on the lab Mac (18 CPUs; the harness gate is 2 %).
  • Fix: the whole table is read every proc_rescan_s. In between, only the watched pids are read: each pinned group's processes, the last window's top-N rows, and the monitor itself.
  • Behaviour:
    • A Process object keeps its cpu_percent baseline, so an unwatched process's value at a rescan averages the interval; a newly busy process appears at the next rescan.
    • The pid → group cache is kept against the full table, so Python command lines are not re-read each rescan.
    • Windows is unchanged: one NtQuerySystemInformation call per window.
  • Result, portable path forced on ww18 (psutil on Windows, ~300 processes): monitor CPU mean 24.9 % → 7.3 % of a core, median 24.8 % → 1.35 %. The rescan windows peak at ~35 %. The Mac number is to be measured on this branch.

Test plan

  • tests/test_host_sampler.py: 52/52 (+18), covering:
    • quantization: grid, nearest rounding, ≤ half-step error, NaN, ints/strings untouched, no mutation, float32-exact;
    • rescan: one full read per period, unwatched pids read only then, pinned pids every window, the watch set, rows equal to a full read.
  • tests/test_pure.py: 40/40
  • webDisplayTools acceptance harness on ww18 (Windows 11, 24 CPUs), typed environment, with this branch's source first on PYTHONPATH (candidate 037a5cc + rig: record the rig-nodes host monitor (/rig/host) in every run's bag webDisplayTools#282): exit 0, every check passes, /rig/host typed 4.0 % of the compressed payload, monitor CPU median 0.9 % of one core
  • Mac (18 logical CPUs, portable path): pixi run test-host 52/52.
    • Standalone sampler, 20 windows, gpu=False, CPU ms per window by part:

      median mean max _processes _core_busy total
      installed 0.3.0 7.1 % 7.0 % 7.5 % 78.6 ms 2.5 ms 82.1 ms
      this branch 0.56 % 1.9 % 7.4 % (the 1-in-5 rescan windows) 18.7 ms 2.6 ms 22.7 ms
    • The process table was the whole macOS cost.

    • (A first harness run on the Mac seemed to show no change; it had loaded the installed 0.3.0, because pixi's ROS activation overwrites PYTHONPATH.)

🤖 Generated with Claude Code

mbreiser and others added 2 commits October 28, 2026 18:42
float32 carries ~24 bits of sampling noise per value, which zstd cannot compress: typed /rig/host
was 6.7 % of a ww18 harness run's compressed bag (1.77 MB/h) against 4.8 % for the String JSON
form (rounded to 3 decimals). host_sampler.quantize rounds every float field to a power-of-two
step before host_monitor.to_msg builds the message, so the low mantissa bits are zero:

- 0..1 fractions and CPU cores (per-core busy, whole-PC cores, process CPU, disk busy, GPU
  utilization, the monitor's own cores): 1/1024 (0.1 %)
- everything else measured (rates, MB, MB/s, ms, GB, °C, W): 1/64

NaN (absent) stays NaN; ints and strings are untouched; the caller's sample is not mutated.
The JSON path is unchanged.

Measured on ww18 (Windows 11, 24 CPUs, ~20 process rows per sample), webDisplayTools
acceptance harness in the typed environment: typed /rig/host 4.1 % of the compressed payload
(1.05 MB/h), every check passing; monitor CPU unchanged at 0.9 % of one core.
tests/test_host_sampler.py: 11 new checks (grid, nearest rounding, ≤ half-step error, NaN,
ints/strings, no mutation, float32-exact) — 45/45.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…watched pids in between

On macOS/Linux (psutil) every window read cpu_percent + memory_info for EVERY process; a
per-process read costs ~0.1-0.2 ms on macOS, so the monitor cost 6.7 % of a core on the lab Mac
(18 CPUs; the webDisplayTools harness gate is 2 %). Now `_ps_rows` reads the whole table every
`proc_rescan_s` (default 5 s, CLI --proc-rescan) and, in between, only the watched pids: every
pinned group's processes + the last window's top-N rows + the monitor. A Process object keeps its
cpu_percent baseline across rescans, so an unwatched process's value at a rescan averages the
whole interval; one that starts using CPU between rescans appears at the next. The pid → group
cache is kept against the full table, so Python command lines are not re-read each rescan.
Windows is unchanged (one NtQuerySystemInformation call reads every process per window).

Measured with the portable path forced on ww18 (psutil on Windows, ~300 processes): monitor CPU
mean 24.9 % → 7.3 % of a core (median 24.8 % → 1.35 %; the rescan windows peak at ~35 %). The
Windows path in the webDisplayTools typed harness: unchanged, median 0.9 %, /rig/host 4.0 % of
the compressed payload. tests/test_host_sampler.py: 7 new checks (one full read per period,
unwatched pids read only then, pinned pids every window, the watch set, rows equal to a full
read) — 52/52; test_pure 40/40.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant