proof-cascade: measure CPU work with the per-process energy counter, which macOS 27 keeps live #120

Merged
dcharlot merged 1 commit from proof-cascade/process-energy into main 2026-10-01 06:21:37 -04:00
Owner

Stacked on #115 (base proof-cascade/cpu-rail-liveness). Merge #115 first; this PR then retargets to main.

On macOS 27 an unprivileged process sees IOReport's CPU energy rails republished only in rare lumps, so #115 makes proof-cascade's --measure decline there. The kernel has another counter it keeps per process, ri_energy_nj from proc_pid_rusage(getpid(), RUSAGE_INFO_V6). That counter is live. It is a public libproc call, with no entitlement and no privilege.

What it measures

  • Domain. The CPU energy the kernel attributes to this process's threads. It does not include other processes, children, GPU, ANE or DRAM. Anything else this process runs during the window is counted.
  • How measure_marginal_uj uses it. work already runs on every spare core in-process, so the counter is read before the workers start and again after they are joined. No idle subtraction is needed, and other processes are excluded by construction.
  • Liveness. It must pass the same sustained-advance probe as IOReport, cached once per process. Each interface checks its own status, so a live process counter can never unlock a frozen IOReport rail.
  • Grade. HwShunt: a vendor/kernel reading that is live. The receipt is still sealed from first and stays ModelBased.

Measured (debug builds, macOS 27.0.1)

M5 Max M3 Ultra
probe status Live (35/35 oracle reads changed) Live (29/29)
cheap closure ×3 355 / 355 / 360 µJ 671 / 676 / 685 µJ
compute 2 ms vs sleep 2 ms 2,732 vs 7 µJ 6,520 vs 2 µJ
sleep beside 4 busy child processes 6 µJ 9 µJ
proof --measure 20,819 µJ/proof, HwShunt, source ProcessEnergy 44,537 µJ/proof, HwShunt

Tests and mutations

  • Tests
    • an independent-oracle FFI pin;
    • a struct-layout pin: 464 bytes, with ri_energy_nj at byte 336;
    • liveness checked against the oracle;
    • stability across runs;
    • sleeping vs computing, including beside busy children;
    • the whole chain with the counter held frozen through a test hook that can only turn the meter off.
  • Mutations caught: reading the parent pid, a constant counter, liveness ignored at selection, and the probe's verdict discarded.

Not covered

  • The figure's accuracy is the kernel's. Nothing checks it against an external meter.
  • GPU-bound work reads low, because the domain is CPU only.

Updates

  • Rebased onto #115's final head: RAPL liveness with every package, the like-for-like headline, and the every-rail IOReport rule. PROCESS_COUNTER dropped the removed every field.
  • Tests are green after the rebase: the workspace at 139 and conformance at 10/10.

🤖 Generated with Claude Code

> **Stacked on #115** (base `proof-cascade/cpu-rail-liveness`). Merge #115 first; this PR then retargets to `main`. On macOS 27 an unprivileged process sees IOReport's CPU energy rails republished only in rare lumps, so #115 makes proof-cascade's `--measure` decline there. The kernel has another counter it keeps per process, `ri_energy_nj` from `proc_pid_rusage(getpid(), RUSAGE_INFO_V6)`. That counter is **live**. It is a public libproc call, with no entitlement and no privilege. ### What it measures - **Domain.** The CPU energy the kernel attributes to this process's threads. It does not include other processes, children, GPU, ANE or DRAM. Anything else this process runs during the window is counted. - **How `measure_marginal_uj` uses it.** `work` already runs on every spare core in-process, so the counter is read before the workers start and again after they are joined. No idle subtraction is needed, and other processes are excluded by construction. - **Liveness.** It must pass the same sustained-advance probe as IOReport, cached once per process. Each interface checks its own status, so a live process counter can never unlock a frozen IOReport rail. - **Grade.** HwShunt: a vendor/kernel reading that is live. The receipt is still sealed from `first` and stays ModelBased. ### Measured (debug builds, macOS 27.0.1) | | M5 Max | M3 Ultra | |---|---|---| | probe status | `Live` (35/35 oracle reads changed) | `Live` (29/29) | | cheap closure ×3 | 355 / 355 / 360 µJ | 671 / 676 / 685 µJ | | compute 2 ms vs sleep 2 ms | 2,732 vs 7 µJ | 6,520 vs 2 µJ | | sleep beside 4 busy child processes | 6 µJ | 9 µJ | | `proof --measure` | 20,819 µJ/proof, HwShunt, source ProcessEnergy | 44,537 µJ/proof, HwShunt | ### Tests and mutations - **Tests** - an independent-oracle FFI pin; - a struct-layout pin: 464 bytes, with `ri_energy_nj` at byte 336; - liveness checked against the oracle; - stability across runs; - sleeping vs computing, including beside busy children; - the whole chain with the counter held frozen through a test hook that can only turn the meter off. - **Mutations caught:** reading the parent pid, a constant counter, liveness ignored at selection, and the probe's verdict discarded. ### Not covered - The figure's accuracy is the kernel's. Nothing checks it against an external meter. - GPU-bound work reads low, because the domain is CPU only. ### Updates - **Rebased onto #115's final head:** RAPL liveness with every package, the like-for-like headline, and the every-rail IOReport rule. `PROCESS_COUNTER` dropped the removed `every` field. - **Tests are green after the rebase:** the workspace at 139 and conformance at 10/10. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
proof-cascade: meter CPU work with this process's own energy counter while it is live
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m52s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 3m38s
standards workspaces build + test / aac-rs (pull_request) Successful in 39s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m36s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 56s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m13s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 47s
standards workspaces build + test / map-rs (pull_request) Successful in 50s
standards workspaces build + test / mesh-rs (pull_request) Successful in 38s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m8s
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
af42bdb8b1
On both Macs that run this suite (macOS 27.0.1, an M5 Max and an M3 Ultra)
IOReport's CPU energy rail is frozen for an unprivileged reader, so the meter
has correctly declined and `proof --measure` has never printed a figure there.
The kernel keeps a live counter of its own: `proc_pid_rusage(getpid(),
RUSAGE_INFO_V6)`'s `ri_energy_nj`, the cumulative CPU energy it attributes to
this process's threads, in nanojoules. It is public libproc API and needs no
privilege. On the M5 Max it changes every ~0.5-0.7 ms under load, holds near
1-2 mJ/s idle, and keeps the energy of threads that have exited; on both Macs
the probe below calls it Live.

Design. `measure_marginal` (new; `measure_marginal_uj` is now its figure)
runs `work` on every spare core and meters it with the first live source, in
order: on macOS this process's counter, then IOReport; on Linux RAPL. For the
process counter there is no bracket and no idle subtraction: the counter is
read before the workers start and after every one has been joined, and the
figure is its advance ÷ every call that completed, so it covers exactly the
calls counted, and other processes are excluded by construction. What the
figure claims, `MeterSource::domain`, says so: this process's CPU energy as
the kernel attributes it to its threads — not other processes, not child
processes, not GPU, ANE or DRAM — and everything this process burns in the
window, so a caller must not run other work in the process while it meters.
It is graded HwShunt (`MeterSource::provenance`; the owner's decision: a
kernel reading of the CPU's own energy counters), and `Measurement::energy()`
builds the schema reading from it.

The counter is used only while it is live, by the same sustained-advance rule
as IOReport and RAPL: through the same probe loop, it must advance in at least
five sample intervals, in at least 80% of them, over at least 300 ms, while
the probe burns 25 ms of thread CPU before each re-read. `process_energy_status`
probes once per process and caches it; `meter_source` admits only `Live`, and
the process rung has its own gate on the cached status besides. The bracket
paths keep their own gates whatever ranks above them: `power_over` now gates
on IOReport's own status and `rapl_power_over` on RAPL's, not on the overall
selection, so a live process counter can never unlock a frozen IOReport rail.
IOReport is not probed by the selection when the process counter is live.

`proof --measure` prints the decision it acted on (`source=`,
`process_energy_status=`, `ioreport_status=`, `rapl_status=`), why each source
not used was not used, the figure with its source and provenance, and its
domain. On this M5 Max, verbatim:

  --measure: hardware meter available=true · source=ProcessEnergy · process_energy_status=Live · ioreport_status=CpuRailFrozen · rapl_status=Unavailable
  measured: 20819 uj / proof  ·  provenance HwShunt (measured_uj present, the schema's strongest tier)  ·  source ProcessEnergy
  receipt: closed at T0:automation · 215 uj · provenance ModelBased · spec Ungrounded · verified true

The receipt is unchanged: still sealed from `first`, the cascade's own
ModelBased figure for the first goal. The CLI's old line "the standard's
headline is now a real hardware reading" is gone: the per-goal figures stay
ModelBased, and it now says so.

Tests. Hardware tests check the meter against an independent oracle — the
tests' own binding of `proc_pid_rusage`, their own `getpid`, a raw 464-byte
buffer read at byte 336, their own loop and literals — and every branch
asserts:
- the FFI pin: three production reads each lie between two oracle reads, and
  advance across 50 ms burns; the struct is 464 bytes with `ri_energy_nj` at
  336, the macOS 27.0 SDK header's numbers;
- `tests/process_energy.rs` (serialized): the probe is Live exactly when the
  oracle sees the counter keep moving, and then it is selected first; a fixed
  cheap closure meters as a non-zero HwShunt figure three times within 3x; a
  closure that sleeps 2 ms meters at under a tenth of one that computes for
  2 ms, also while four busy child processes burn CPU beside it;
- `tests/process_energy_frozen.rs` drives the whole chain with the counter held
  (`simulate_frozen_process_energy`, a hook that can only turn the meter off):
  Frozen, never selected, no figure, the work never runs, one probe run;
- `proof --measure` (proof-cli test): the source is the first live one in
  order, every printed reason is its printed status's own, the receipt equals
  the first goal's row and stays ModelBased, and where the test process's own
  counter keeps moving the CLI must have printed a non-zero HwShunt
  ProcessEnergy figure with its domain;
- pure, on every platform: the probe composition on scripted counters (live at
  the twelfth 25 ms interval; constant, one-, two- and four-step counters
  Frozen; unreadable Unavailable), the measurement's arithmetic and read order
  on a scripted counter each call raises by exactly 7,000 nJ (exactly 7 uJ; it
  holds only if the counter is read before the first call and after the join),
  rounding, a falling or unreadable counter giving no figure and an unreadable
  one never running the work, the macOS order (IOReport not even asked when the
  process counter is live), and the grading and domain of every source.
The IOReport tests now assert on IOReport's own gate (`bracket_admitted`,
`bracket_marginal_uj`), and `ioreport_once.rs` / `ioreport_unopenable.rs` cover
both probes (each runs once per process; with no Energy Model group and the
counter held, every meter declines).

Measured, verbatim from the test logs (debug builds, macOS 27.0.1):
- M5 Max: `process liveness: status=Live source=Some(ProcessEnergy) oracle=Oracle
  { moved: true, reads: 35, changes: 35, advanced_nj: 1061927078 }`; cheap
  closure, three calls: 355, 355, 360 uJ; compute 2 ms = 2732 uJ, sleep 2 ms =
  7 uJ, sleep 2 ms beside 4 busy children = 6 uJ.
- M3 Ultra (the lib and process_energy test binaries and `proof`, built from
  this commit's code before its last doc-comment and README edits, copied to
  /tmp on 10.10.10.2, run once with --nocapture, deleted): `process liveness: status=Live
  source=Some(ProcessEnergy) oracle=Oracle { moved: true, reads: 29, changes:
  29, advanced_nj: 5293659397 }`; cheap closure 671, 676, 685 uJ; compute 2 ms
  = 6520 uJ, sleep 2 ms = 2 uJ, beside 4 busy children = 9 uJ; `measured: 44537
  uj / proof · provenance HwShunt · source ProcessEnergy`; 88/88 lib tests and
  3/3 process_energy tests passed; IOReport there `CpuRailFrozen` as before.

Falsified, each restored through git before the next (proof-cascade and
proof-cli test suites):
- reading the parent's pid: the FFI pin fails ("round 1: the production read
  177161340 is outside the oracle's [897071438, 897071438]"), and so do the
  liveness test ("the probe says Frozen; the oracle saw the counter change at 35
  of 35 reads"), both metering tests and the CLI test ("this test's own process
  counter changed at 24 of 24 reads, yet the CLI metered with none");
- a constant counter: the same five tests ("the production read 123456789 is
  outside the oracle's [...]");
- liveness ignored at the selection (the counter chosen whenever it reads): the
  frozen-chain test ("a held process counter was selected"), the unopenable test
  and the pure ordering test fail;
- the probe run but its verdict discarded (always Live): the frozen-chain and
  unopenable tests fail ("a held counter was called Live").

`cargo test --workspace` and the proof-conformance binary pass; `cargo check
--workspace --tests` and `cargo clippy --workspace --all-targets` for
x86_64-unknown-linux-gnu are clean; clippy on macOS reports only the two
IOReport FFI lints the README already lists.

Not done: the counter's accuracy is the kernel's; nothing here checks it
against an external meter. Its domain is CPU only, so GPU-bound work would
meter low, and a caller that runs other threads in the process while it
meters is charged for them. The process figure resolves 1 uJ per call;
`Some(0)` from it means under half a microjoule, and the CLI says so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot force-pushed proof-cascade/process-energy from af42bdb8b1
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m52s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 3m38s
standards workspaces build + test / aac-rs (pull_request) Successful in 39s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m36s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 56s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m13s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 47s
standards workspaces build + test / map-rs (pull_request) Successful in 50s
standards workspaces build + test / mesh-rs (pull_request) Successful in 38s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m8s
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
to 0e9e3c8f6d
All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m8s
standards workspaces build + test / aac-rs (pull_request) Successful in 45s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m7s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 51s
standards workspaces build + test / map-rs (pull_request) Successful in 43s
standards workspaces build + test / mesh-rs (pull_request) Successful in 37s
standards workspaces build + test / openpay (pull_request) Successful in 1m48s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m52s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m18s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m15s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m10s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m37s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m45s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 3m16s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m48s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 3m11s
standards workspaces build + test / sense-rs (pull_request) Successful in 3m10s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 2m43s
2026-09-30 21:41:49 -04:00
Compare
dcharlot changed target branch from proof-cascade/cpu-rail-liveness to main 2026-10-01 06:21:17 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!120
No description provided.