SIGN IN SIGN UP

fix: sample IOReport continuously and poll local mode at its own cadence (#286)

Follow-up to #285. While checking why the history graph's time axis was uneven, two defects turned up in the local-mode collection path that compounded each other.

## Problem 1: IOReport opened its own measurement window

`IOReport::get_sample` took a sample, slept 100ms, took a second one, and returned the delta. `collect_once` averaged four of those. So each collection blocked its caller ~495ms and observed 400ms of wall time. At the effective 3.5s period that is **14% of elapsed time**: whatever happened in the other 86% never reached the graphs. The blocking also ran directly inside a `tokio::join!` arm in `local_collector.rs:280-290`, while the process collection immediately below it uses `spawn_blocking` for exactly this reason.

## Problem 2: local mode asked for the remote cadence

`run_local_mode` called `EnvConfig::adaptive_interval(1)`. The `1` reads as "one host" but selects the `1..=10 => 3` remote-nodes arm, so local mode polled at 3s and the `node_count == 0` arm, commented `// Local monitoring only (no remote nodes)`, was unreachable. `main.rs:332,344` assumed `unwrap_or(2)` for the same run, so one binary carried three different local defaults.

## Change

**Continuous delta.** Every subscribed channel is a cumulative counter (energy in the Energy Model group, residency ticks in CPU/GPU stats), so a delta between any two samples is exactly the activity between them. `get_sample_since_last` retains the newest sample and differences the next call against it: no sleep, one `IOReportCreateSamples` per poll, and a window equal to the full polling interval. The `prev_sample` field this needs was already declared on `IOReport` but had never been written to. Deltas below `MIN_DELTA_WINDOW` (50ms) report no result instead of dividing counters by a near-zero interval, and leave the baseline in place so a fast caller still accumulates one. `get_sample` stays for the first collection of a session. Averaging four samples is gone: one long delta already is the interval's time average.

**Named local cadence.** Local call sites use `EnvConfig::local_interval()` and never spell out a node count, which removes the class of bug rather than the instance. `main.rs`'s two `unwrap_or(2)` sites use it too.

**Uniform cache.** The 5s window for the first ten `collect_once` calls existed to absorb the old blocking. It also made the first ~10 seconds of every history graph a staircase of repeated values. Replaced by a single `CACHE_DURATION_MS`, which still dedupes the several readers that run within one collection cycle.

## Measurements

M1 Ultra, release build, 200x50 tmux, 60s per run.

Collection cost in isolation:

| | before | after |
|---|---|---|
| collection | 495ms | **24ms** |
| worker occupancy @1s | 33.0% | **2.4%** |
| IOReport samples/cycle | 8 | **1** |
| observation coverage @3s | 14% | **100%** |

Full TUI CPU by interval:

| interval | before | after |
|---|---|---|
| 1s | 8.16% | 4.71% |
| 2s | 5.32% | 3.47% |
| 3s | 4.06% | 2.13% |

End to end, default local mode with no `-i`: **4.06% at 3s before, 3.52% at 1s after.** The sample rate triples at equal or lower cost, which is what makes problem 2's fix affordable.

## Correctness check

A/B of both code paths over the same period, to confirm this changes cost and coverage rather than the numbers:

```
blocking400  window=410ms  cpu_power=2.41W gpu_power=0.83W gpu_res=24.5% p_res=2.4%
blocking400  window=407ms  cpu_power=8.14W gpu_power=0.93W gpu_res=28.3% p_res=6.1%
blocking400  window=410ms  cpu_power=9.92W gpu_power=0.84W gpu_res=28.7% p_res=4.0%
continuous   window=1012ms cpu_power=2.63W gpu_power=0.77W gpu_res=25.4% p_res=1.5%
continuous   window=1012ms cpu_power=3.54W gpu_power=0.78W gpu_res=25.1% p_res=3.3%
continuous   window=1010ms cpu_power=19.29W gpu_power=0.79W gpu_res=24.6% p_res=14.9%
```

Every metric lands in the same range. CPU power is spiky in both, which is the metric's nature.

## Tests

- Added `test_local_interval_is_the_no_remote_nodes_arm`: the local cadence must equal `adaptive_interval(0)`, must differ from `adaptive_interval(1)`, and must be at most 2s.
- Verified by hand that a sub-`MIN_DELTA_WINDOW` call returns no delta and leaves the baseline armed for the next call.
- `cargo test` fully green, `cargo clippy --all-targets` clean, `cargo fmt` applied.

## Scope note

macOS Apple Silicon only for the sampling change; the interval fix applies to every local-mode platform (3s to 2s off Apple Silicon, matching what `main.rs` already assumed).
J
Jeongkyu Shin committed
18fb5950a496176a927f2a7cbdc1bca2ff74c59e
Parent: 517e849
Committed by GitHub <noreply@github.com> on 7/27/2026, 5:23:51 AM