sandbox-meter: a live per-process CPU energy probe for in-process work, and a real reading for a direct child #121
Loading…
Reference in a new issue
No description provided.
Delete branch "sandbox-meter/process-energy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
On macOS 27 an unprivileged process sees IOReport's CPU rails republished only in lumps, so #114 makes
PowerMeterfall back to its estimator there. The kernel keeps a per-process counter,ri_energy_njfromproc_pid_rusage(getpid(), RUSAGE_INFO_V6), and that one is live. It is a public libproc call, with no privilege.The coverage trap, and how it is avoided
The counter covers this process's own threads, not child processes.
PowerMetermeters subprocess work for jcp-gateway (the upstream server), joule-code-remediate (cargo) and sandbox-proof. In those places a per-process figure would quietly cover only the parent. So:ProcessEnergyProbe(Cpu, HwShunt, liveness-gated by the same sustained-advance rule) is never indetect(). It comes only fromPowerMeter::detect_in_process(). Where the counter is live, that is this probe alone, so CPU is never counted twice; otherwise it isdetect().Host::handle→run_interaction→ handler, all on the worker thread, nothing spawned) and the joule-ui-interaction examples. jcp-gateway, remediate and sandbox-proof stay ondetect(), and their docs say why.run_attributedgets a real per-child reading where it can. It waits for the child to exit without reaping it (waitid(P_PID, pid, WEXITED|WNOWAIT)), then readsproc_pid_rusageon the child. The newAttribution::child_energy_ujis set only when the counter is live, the child reaped no CPU-using descendant, and the reading advanced. cargo→rustc (grandchildren) getsNone. The existing apportionment is unchanged.Measured (macOS 27.0.1)
Live(39/39 intervals)Live(40/40)CpuRailFrozenCpuRailFrozenServed joule-ui interactions now seal real HwShunt figures (~23–26 mJ each). Release-mode
jcp_meteredallows 81 HwShunt interactions, then the budget binds and the 82nd is denied.Mutations caught (7):
detect();served_power_meterreturningdetect().Suites green: sandbox-rs 238, joule-ui 72, jcp 33, remediate 13. The joule-ui vectors regenerate byte-identical.
Updates
package-Nzones, sandbox-exec-microvm's gated RAPL/MSR meters, and the microVM held-counter fix.🤖 Generated with Claude Code
joule-ui-serve's action handlers and the joule-ui-interaction examples' handlers are in-process work, verified rather than assumed: `Host::handle` calls the handler inside `run_interaction`, on the worker thread that holds the host lock, and nothing in joule-ui-serve or the examples spawns a process. So they now meter with `PowerMeter::detect_in_process()`, and on a host whose per-process CPU energy counter is live (both Apple Silicon hosts checked, macOS 27.0.1, where the machine-wide CPU rail is frozen) each interaction is sealed as a `HwShunt` reading of the energy the kernel attributes to this process's threads over the interaction, instead of the 20 W wall-clock estimate. Where the counter is not live the meter is `detect()`'s, as before. The figure is this process's, not one thread's: in joule-ui-serve it includes any worker that served a socket during the span. joule-ui-serve: `main` serves with `served_power_meter()`, which is `detect_in_process()`, and the end-to-end test now builds its meter from that same function and asserts, where the counter is live, that the meter is the process probe and that each ~24 ms served interaction is `HwShunt`. The examples: `real_meter` and `jcp_metered` use `detect_in_process()` and say which counter they read. With real readings an interaction of ~150 ms costs a fraction of a joule, not the estimate's 3 J, so six fires no longer spend `jcp_metered`'s 15 J grant; it now fires until the real JCP runtime denies one (at most 200), printing the first five and the last three, so the budget binds whatever the figures are. The grant itself is unchanged. SPEC §5 names the per-process counter among the reference meter's sources; the two Cargo comments say which meter each crate uses. Tests: joule-ui-serve, -interaction, -conformance, -mcp, -core, -a2ui and -render pass (72). The served interactions on this M5 Max: `Energy { measured_uj: Some(25908), ... provenance: HwShunt }` and `Some(23193)`. `gen_vectors` regenerated into a scratch directory is byte-identical to `conformance/v1/` (`diff -r` clean). The examples, release build, here: real_meter: render #1: allow · 195798 µJ [HwShunt] ... sealed chain: 3 links verified, 584156 µJ total — hardware counter deltas (HwShunt), this process's CPU energy over each interaction jcp_metered: 82 interactions sealed: 81 allowed; 15001229 µJ sealed, hardware counter deltas (HwShunt) from process-energy; 15000000 of the 15000000 µJ budget debited; 1 denied by the real JCP runtime once the budget was spent. (The per-interaction figure varied from 0.04 to 0.21 J: the handlers burn for 150 ms of wall time, and other builds were loading this machine.) Falsified, restored from git and re-run green: `served_power_meter` returning `detect()` fails `the_served_meter_never_seals_zero_or_an_estimate_as_a_reading`. Co-Authored-By: Claude Opus 5.5 <[email protected]>d66407088b542ef4b522542ef4b522bf4f450876bf4f45087602a833023f02a833023f8aa25541b8