open-standards/joule-code/joule-code-rs/crates/joule-code-remediate/src
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science ad8fbf48ca sandbox-meter: this process's own CPU energy, read only for in-process work and only while it keeps advancing
On macOS 27.0.1 an unprivileged reader sees the machine-wide IOReport CPU rail
frozen, so every meter here has been an estimator on Apple Silicon. The kernel
does keep a live per-process figure: `proc_pid_rusage(getpid(),
RUSAGE_INFO_V6)` and its `ri_energy_nj`, the CPU energy it attributes to this
process's threads, public libproc, no privilege. On this M5 Max it advances
every few milliseconds under load.

Its coverage is the trap. It counts this process's own threads and nothing
else: not child processes (the kernel never folds a child's energy into its
parent, not even after reaping it; `rusage_info` has a child CPU-time field
and no child energy field), not other processes, not the GPU or the ANE.
`PowerMeter` meters subprocess work in jcp-gateway (the upstream MCP server),
joule-code-remediate (cargo) and sandbox-proof (the engine), and in-process
work in joule-ui-serve's handlers. A probe added to `detect()` would read the
gateway waiting, near zero, as the energy of the tool call.

So the new `ProcessEnergyProbe` (`Cpu`, `HwShunt`, source "process-energy") is
never part of `detect()`. It is reached only through a new constructor,
`PowerMeter::detect_in_process()`, documented as only for work on this
process's own threads: where the counter is live the meter reads it and
nothing else (a machine-wide CPU probe beside it would count the same work
twice); where it is not, and off macOS, it is `detect()`. The counter is gated
by the shared rule, probed once per process and cached
(`process_energy_status()`, a `CounterStatus`): it must advance in at least
five intervals, at least four in five, over at least 300 ms, while the probe
burns CPU, through the same `liveness::counter_run` and `gated` as RAPL and
NVML. `Span::stop`'s held-CPU rule applies unchanged: a span over which the
counter did not advance is estimated. The struct is the SDK's
`rusage_info_v6` field for field, pinned by a test at the SDK's own sizeof and
offsets (464 bytes, `ri_energy_nj` at 336).

`run_attributed` (subprocess windows). The question was whether a child's own
figure can be read reliably. It can, for the direct child: wait for the exit
without reaping (`waitid(P_PID, pid, WEXITED | WNOWAIT)`), `proc_pid_rusage`
the exited child, then reap with `wait4` as before. Measured here before
writing it: the read succeeds on the zombie and is stable across repeated
reads, then fails (ESRCH) once reaped; `sh -c 'awk ...'` (sh execs awk) read
1.647 J for 0.6 s of awk's CPU; `sh -c 'awk ...; true'` (awk a reaped child of
sh) read 5.5 mJ, sh's own start-up, beside 0.63 s of reaped-descendant CPU
time. So the new `Attribution::child_energy_uj` is the direct child's own
energy, given only when the per-process counter is live, the child reaped no
descendant that used CPU time, and it advanced. Grandchildren are not covered:
cargo -> rustc gets `None`, as does any shell that forks its commands, and a
descendant the child did not reap leaves no trace at all. The status is read
before the window opens, so its first-use probe never lands inside it. The
apportioned `attributed_uj` is unchanged.

jcp-gateway, joule-code-remediate and sandbox-proof stay on `detect()`, and
their docs now say why: their metered work is a subprocess. remediate's test
helper sets the new field to `None` (cargo).

Tests. Hardware, in their own process (`tests/process_energy.rs`, one at a
time, each against an oracle that reads `ri_energy_nj` into a raw buffer at the
SDK offset and shares no code with the probe):
`the_process_energy_probe_is_live` (pinned Live on Apple Silicon; the oracle
saw 39/39 intervals advance; the probe ran once),
`an_in_process_burn_is_a_hardware_reading_of_its_energy` (200 ms of one
thread's CPU, user-interactive QoS, via `detect_in_process`: one `Cpu`
`HwShunt` record in 0.2..20 J and inside the oracle's reading around it),
`an_in_process_sleep_reads_under_a_tenth_of_a_joule`,
`detect_does_not_include_the_process_probe`,
`work_in_a_child_process_is_not_attributed_to_the_in_process_probe`. Unit:
`the_rusage_struct_is_the_sdks_layout`,
`this_process_reads_itself_and_a_missing_pid_reads_nothing`,
`a_childs_own_energy_is_a_reading_only_for_a_single_process_child`,
`only_a_live_status_builds_the_process_probe`,
`detect_never_reads_this_process_counter` (every status combination),
`detect_in_process_reads_this_process_only_for_a_live_status` (and never calls
the fallback when it reads), `a_single_process_childs_own_energy_is_read_before_it_is_reaped`,
`a_child_whose_work_ran_in_a_descendant_has_no_reading`.

Measured on this M5 Max (macOS 27.0.1, other builds loading it), verbatim:
  200 ms in-process burn: 0.4361 J over 219.588375ms wall (oracle around it: 0.4361 J)
  200 ms in-process sleep: 575 µJ
  child work: child 0.8730 J, this process 0.001304 J around the wait, span [... measured_uj: Some(1304) ... HwShunt]
  process energy: status=Live oracle advanced in 39/39 intervals, probe runs=1
  child energy: status=Live child_energy_uj=Some(3128510) proc_cpu_secs=1.499
On the M3 Ultra (macOS 27.0.1, 10.10.10.2; lib and process_energy test
binaries copied, run once with --nocapture, deleted), verbatim:
  200 ms in-process burn: 1.3150 J over 200.032666ms wall (oracle around it: 1.3150 J)
  200 ms in-process sleep: 1796 µJ
  process energy: status=Live oracle advanced in 40/40 intervals, probe runs=1
  child work: child 2.3750 J, this process 0.014451 J around the wait, span [... measured_uj: Some(14451) ... HwShunt]
  child energy: status=Live child_energy_uj=Some(10145217) proc_cpu_secs=2.044
  ioreport liveness: status=CpuRailFrozen oracle=CpuRailMoved(false) selected=false
  105 lib tests and 5 process_energy tests passed.
The per-process counter is live on both chips; IOReport's CPU rail stays frozen.

sandbox-meter passes (105 lib, 5 process_energy, both once-per-process
files); every sandbox-rs crate that depends on the meter passes (238 tests);
jcp-gateway and jcp-action-receipt (33) and joule-code-remediate (13) pass.
Clippy is clean for aarch64-apple-darwin and x86_64-unknown-linux-gnu, and
x86_64-apple-darwin checks clean.

Falsified, each restored from git and re-run green: the probe added to
`detect()` fails `detect_never_reads_this_process_counter` and
`detect_does_not_include_the_process_probe`; a constant counter, and the
parent's pid in place of this one, fail `the_process_energy_probe_is_live`
(status Frozen while the oracle saw it advance) and the 200 ms burn (it falls
back to the 4.1 J estimate); `given` ignoring the status fails
`only_a_live_status_builds_the_process_probe` and
`detect_in_process_reads_this_process_only_for_a_live_status`; a child reading
that ignores reaped descendants fails both child tests (sh's 5.5 mJ stood in
for awk's work); one that ignores liveness fails the pure child test.

Rebased onto 7cf1f036 (the RAPL figure sums the package zones by name),
which builds `rapl` under test on every platform and adds its fake-tree
tests. The one conflict was the module declarations in probe/mod.rs; both
`process` and `rapl`'s new cfg stand. The counts above predate the rebase:
on the M5 Max sandbox-meter now has 120 lib tests (114 before it), all
passing, with process_energy (5) and both once-per-process tests; the
sandbox-rs workspace, the sandbox-conformance binary, jcp-gateway,
jcp-action-receipt and joule-code-remediate pass too. Clippy on
sandbox-meter is clean for aarch64-apple-darwin and
x86_64-unknown-linux-gnu. The M3 Ultra run was not repeated.

Rebased again, onto a16e36cf, whose sandbox-meter side builds `liveness`
and `rapl` on every platform and adds `RaplPackages` and
`counter_liveness`. The one conflict was again the module declarations in
probe/mod.rs: `liveness` and `rapl` are unconditional, `process` stays
macOS-only. On the M5 Max sandbox-meter now has 122 lib tests, all passing,
with process_energy (5) and both once-per-process tests; the sandbox-rs
workspace (321), the sandbox-conformance binary, jcp-gateway and
jcp-action-receipt (33) and joule-code-remediate (13) pass. Clippy on
sandbox-meter and sandbox-exec-microvm is clean for aarch64-apple-darwin and
x86_64-unknown-linux-gnu.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 23:01:05 -04:00
..
bin joule-code-remediate: the description grades its joules, and the z3 tests never pass silently 2026-09-30 20:10:38 -04:00
lib.rs sandbox-meter: this process's own CPU energy, read only for in-process work and only while it keeps advancing 2026-09-30 23:01:05 -04:00