redbear-power: v1.42 CPU affinity

The next item from the v1.41 deferred list: read
/proc/<pid>/status:Cpus_allowed_list and display it as
both a single-char row indicator and a full expanded
list in the PID detail popup. htop parity.

Kernel format
  The kernel emits the list as comma-separated ranges:
    "0-3,5,7-11" means CPUs 0, 1, 2, 3, 5, 7, 8, 9,
    10, 11
  Cpus_allowed_list is the HARD affinity mask (settable
  via sched_setaffinity(2)). v1.42 reads it because it
  matches what an operator sees with 'taskset'.

New functions
  - read_cpu_affinity(pid): parses the kernel string
  - parse_cpu_list(s): public, testable parser
  - format_cpu_list(ids): inverse of parse_cpu_list
  - read_cpu_affinity_for_pid(pid): pub wrapper for the
    PID detail popup

Two display modes
  - Process panel row: '*' (subset), ' ' (all CPUs),
    '?' (unknown). Single char so COMM stays visible.
  - PID detail popup: full range string + expanded
    Vec (truncated to 8 items on large machines).

New field on ProcessInfo
  - cpu_affinity: Option<Vec<u32>>

Robustness
  - Whitespace tolerated
  - Out-of-order or duplicate IDs deduped and sorted
  - Non-numeric chunks silently dropped
  - Reversed ranges (start > end) silently dropped
  - Empty input returns empty Vec (popup distinguishes
    'no data' / None vs 'explicitly empty' / Some(empty))

Tests
  - 13 new tests (11 in process.rs for parse/format/
    read, 1 self-affinity test, 1 missing-pid test).
  - 183/183 tests pass (was 170 in v1.41).

The improvement plan doc is also updated with §66
covering the v1.42 architecture, kernel format, the
two display modes, the parse/format inverse pair, and
the v1.43 deferred list.
This commit is contained in:
2026-06-21 13:38:24 +03:00
parent 57a3ea6c9e
commit 0771fa2ff6
3 changed files with 448 additions and 2 deletions
@@ -5697,6 +5697,99 @@ dir within a single refresh cycle.
per-thread, so this would be a synthetic derivation.
Defer to v1.42 if user demand appears.
## 66. v1.42 CPU Affinity (2026-06-21)
The next item from the v1.41 deferred list: CPU affinity
from `/proc/<pid>/status:Cpus_allowed_list`. htop has
this as a column; v1.42 ships it as both a single-char
row indicator (Process panel) and a full expanded list
(PID detail popup).
### 66.1 Kernel format
The kernel emits the list as comma-separated ranges:
```
0-3,5,7-11 means CPUs 0, 1, 2, 3, 5, 7, 8, 9, 10, 11
```
`Cpus_allowed_list` is the **hard** affinity mask
(settable via `sched_setaffinity(2)`). The kernel also
exposes `Cpus_allowed` (same format, but only the
effective subset — without isolated CPUs that exist
but aren't allowed for this process). v1.42 reads
`Cpus_allowed_list` because it matches what an operator
sees when they set the affinity with `taskset`.
### 66.2 Two display modes
| Location | Format | Why |
|----------|--------|-----|
| Process panel row | `*` (subset) / ` ` (all CPUs) / `?` (unknown) | Single char so it doesn't push COMM off the visible area. |
| PID detail popup | Full range string + expanded list | Operators debugging thread pinning need the exact list. |
The `*` indicator fires when the affinity list is
**shorter** than the host's CPU count. We can't compare
specific IDs (host CPUs may have non-contiguous IDs on
NUMA systems) so the comparison is count-based. On
machines with hot-pluggable CPUs, the host CPU count
changes over time, and the indicator might briefly show
`*` for a process with the full mask. The popup shows
the truth.
### 66.3 `parse_cpu_list` and `format_cpu_list`
Inverse pair, both `pub` for testability:
```rust
parse_cpu_list("0-3,5,7-11") == [0, 1, 2, 3, 5, 7, 8, 9, 10, 11]
format_cpu_list(&[0,1,2,3,5,7,8,9,10,11]) == "0-3,5,7-11"
```
Robustness:
- Whitespace tolerated (`" 0-3 , 5 "` parses correctly)
- Out-of-order or duplicate IDs are deduped and sorted
- Non-numeric chunks silently dropped (kernel never
emits these, but a corrupt procfs might)
- A range with start > end silently dropped
- Empty input returns empty Vec (popup distinguishes
"no data" / None vs "explicitly empty" / Some(empty))
### 66.4 Tests
| Test | What it verifies |
|------|------------------|
| `parse_cpu_list_basic` | Single range expands correctly. |
| `parse_cpu_list_mixed_ranges_and_singletons` | The canonical mixed format. |
| `parse_cpu_list_handles_whitespace` | Whitespace tolerated. |
| `parse_cpu_list_dedupes_and_sorts` | Out-of-order and duplicate IDs are deduped. |
| `parse_cpu_list_silently_drops_malformed_chunks` | Non-numeric and reversed ranges dropped. |
| `parse_cpu_list_empty_returns_empty` | Empty input returns empty Vec. |
| `format_cpu_list_basic` | Contiguous range collapses to "start-end". |
| `format_cpu_list_mixed` | Mixed ranges and singletons. |
| `format_cpu_list_empty` | Empty input returns "". |
| `format_cpu_list_single_id` | Single CPU ID renders without range dash. |
| `parse_and_format_round_trip` | parse → format produces the original kernel string (4 cases). |
| `read_cpu_affinity_handles_self` | Test runner's own affinity is non-empty + sorted. |
| `read_cpu_affinity_returns_none_for_missing_pid` | None for non-existent PID. |
**183/183 tests pass as of v1.42.**
### 66.5 What was NOT changed (intentional)
- **History reclaim LRU** — defer to v1.43. Even at
thousands of short-lived procs, each `VecDeque<u8>` is
~24 bytes; the LRU cap is a "polish" feature, not a
"prevents OOM" feature.
- **Per-thread CPU%** (synthetic) — defer to v1.43 if
user demand appears. The Linux kernel only exposes
process-total CPU%, not per-thread.
- **CPU affinity setter (taskset-style keypress)** —
defer to v1.43. The reader side is in v1.42; the
writer side requires an ioctl wrapper that we don't
have yet.
## See Also
- **`local/docs/RATATUI-APP-PATTERNS.md`** §13 — the canonical ratatui 0.30 best-practices update that this plan is derived from. Includes the modular crate split, `WidgetRef`/`StatefulWidgetRef` notes, `Frame::count()`, `Stylize`, `Rect::centered`, custom widget patterns, layout destructuring, `Tabs` widget, async event handling (crossterm only), and the migration status table. Use this as the implementation guide while this doc is the roadmap.