`RaplCounter` read one file, `/sys/class/powercap/intel-rapl:0/energy_uj`, and
its liveness probe watched that one file, cached per path. On a multi-socket
server that is package 0's energy alone, graded `HwShunt` for the domain
`CpuPkg` while every other socket's energy is missing. And nothing makes zone 0
a package: `intel-rapl:<n>` is only an identifier, and each zone says what it
is in its `name` file, so a host whose zone 0 is the platform zone (`psys`) or
a `dram` zone had that domain read and probed as the package. proof-cascade
does this right since 9334ec0c; eoc-meter now does too (26165a77); this is the
same rule here.
- The package set is listed from the powercap directory: every top-level
`intel-rapl:<n>` zone whose `name` reads `package-<digits>`. Left out wherever
they sit, zone 0 included: `psys` (the platform zone, whose energy includes
the packages', so summing it beside them counts them twice), `dram`, the
`core`/`uncore`/`dram` sub-zones (`intel-rapl:0:0`), the control type, and
the MMIO interface (`intel-rapl-mmio:0`, also named `package-0`, which
republishes a package). The listing reads the directory, not fixed indices.
- Liveness: one channel per package, and the shared rule asks every channel to
be live, so RAPL is live only when EVERY package keeps advancing while the
probe burns CPU. A host with no package zone (only `psys`) is `Unavailable`
with nothing opened or burned.
- The cache is per package set, keyed by the package zones' directories, not
per file path. `RaplCounter::new()` lists the set and reuses a verdict for
the same set; the counter sums exactly the packages that were probed.
- `read()` returns a running total: the first read's package counters summed,
then each package's delta added, each taken modulo its own
`max_energy_range_uj`. Summing the raw counters would make one package's
wrap a fall of the sum, and with sockets at different ranges no single range
is right. Before this, a wrap of the one zone was a fall, which `jcr1_mint`
and the like take as no reading; now a wrap is read across. The files are
read under the counter's lock, so two threads' reads are folded in the order
they were taken, and an older read is never taken for a wrap. A package that
cannot be read fails the read (`Platform`): no figure that leaves a package
out. A package whose counter falls while its range is unknown (absent, or 0)
makes that read `CounterWrap`, and every later one too, because the total
would be short by energy no one can know.
- The read path is no longer compiled only for Linux: off Linux the counter is
still never available (`rapl_liveness` is `Unavailable` there, with no
probe), but the code it would run is the code the tests run on a Mac.
API: `RAPL_ENERGY_PATH` and `RaplCounter::with_path(&'static str)` are gone, as
a single `energy_uj` path cannot say which domain it is or stand for every
package; `RaplCounter::with_root(dir)` takes a powercap directory instead (a
container or chroot that mounts sysfs elsewhere), and `POWERCAP_ROOT` names
the default. `rapl_liveness()` takes no argument: it is this host's package
set. Nothing in the monorepo called the removed items. Docs that said a RAPL
figure is one file are corrected (`liveness.rs`, `detect_meter`, and the
`jcr1_mint` example's header).
Tests, on fake powercap directories under the temp dir (removed when done), so
they run on macOS:
- a two-socket server (the control type, package-0 and package-1 with core and
dram sub-zones, psys, a top-level dram, intel-rapl-mmio:0) lists packages 0
and 1 only, and with every zone moving the delta is the two packages' energy
(250,000 + 400,000 uJ) and nothing else;
- zone 0 named psys beside package zone 1: zone 1 alone is listed, sampled and
summed;
- per-zone wrap: package 0 wraps at 262,143,328,850 while package 1 advances,
then package 1 wraps at 65,532,610,987: deltas of 1,700 and 2,500 uJ; taken
at the other zone's range, package 1's wrap would be off by the difference
of the two ranges;
- a package with no range, absent or 0, is summed while it rises; its fall is
`CounterWrap`, and so is the next read;
- an unreadable package fails the read and the opener;
- an only-psys host, an empty directory and a missing one: no package, the
real probe returns `Unavailable` in under 100 ms (no burn), and
`RaplCounter::with_root` is unavailable and reads `NoCounter`;
- scripted: two packages advancing are Live, one held is Frozen;
- the cache: once per package set, and a set with one more package is another
set; the key of a listed tree is its package directories;
- the production probe (real reads, real CPU burn, real clock) on a two-socket
tree: held packages are Frozen after the whole budget; both packages advanced
by a writer thread are Live inside it; package 0 and psys advanced while
package 1 holds is Frozen.
Falsified, each mutant applied to this commit, run, and restored with
`git checkout` (blob hash equal to HEAD's):
- back to `intel-rapl:0` only, whatever its name: 8 of the 14 RAPL tests fail;
- every top-level zone summed (no name check): 5 fail;
- every package wrapped at package 0's range: 1 fails, the per-zone wrap test;
- the probe watching the first package alone: 1 fails, the production probe
with package 1 held;
- the cache keyed by the first package's path alone: 1 fails.
What ran where, on this M5 Max (macOS 27.0.1): jouleclaw-energy `--features
rapl` 48 lib + 2 integration tests pass, `--features ioreport,rapl,nvml` 111 +
2, default features 34 + 2; mesh-meter 2 by default, 10 with
`--features hw-meter-rapl`, 12 with `--features hw-meter-apple`;
jouledesk-github 12; the `jcr1_mint` example compiles. For
x86_64-unknown-linux-gnu, `cargo check --all-targets --features rapl` and
`cargo clippy --all-targets` with `rapl` and with `rapl,nvml` pass with no
warning in a file this touches (clippy's existing ones are in jouleclaw-core,
the ledger tests and the IOReport FFI), and mesh-meter's clippy with
`hw-meter-rapl` is clean. The sysfs reads under the real powercap directory
and the Linux-only cached probe are type-checked, not run: there is no Linux
host with readable RAPL here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>