a process reads its own energy only once the reading thread has left its core #229
Merged
dcharlot
merged 5 commits from 2026-10-13 02:22:46 -04:00
fix/in-process-self-read-fold into main 2026-10-13
eoc annex: the per-process counter's lumps are the kernel's fold, and a checked read is exact
Some checks failed
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 44s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m20s
standards workspaces build + test / aac-rs (pull_request) Successful in 2m39s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 1m26s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 56s
standards workspaces build + test / map-rs (pull_request) Successful in 56s
standards workspaces build + test / mesh-rs (pull_request) Successful in 53s
standards workspaces build + test / openpay (pull_request) Successful in 3m12s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 54s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m33s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 1m1s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 4s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 4s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m17s
standards workspaces build + test / jcp-rs (pull_request) Failing after 42s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m58s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m12s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m11s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m17s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m14s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m26s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m52s
The annex reported that the macOS per-process energy counter "accrues in lumps" under sub-millisecond sampling, and left it there. The mechanism is now established: a thread's energy is added to its process's count when the thread leaves its core, so a reader on a running thread sees nothing until its quantum ends. A reader that first leaves its core and checks its own thread's count moved reads 1 ms spans to within a few percent of an independent read, where unchecked it read none of them on an idle host. Say so beside the measured figures, which stand. Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12
proof-cascade: the meter reads its own process only once the measuring thread has left its core
measure_marginal's process-counter rung brackets its window with two reads of this process's CPU energy (proc_pid_rusage ri_energy_nj). The kernel adds a thread's energy to that count only when the thread leaves a core, and a process's read of its own count does not take the reading thread off its core. So the first read left up to a scheduling quantum of what the measuring thread spent before the window (the liveness probe's burn, the caller's own work) to land inside it, and the figure is sealed HwShunt. Measured on an M3 Ultra (macOS 27.0.1) with the meter's own read at each end of a window on the reading thread, against the tests' own reader, which sleeps before each read until the process's context-switch count rose; three runs of 40 windows of each kind: - 1 ms of the reading thread's CPU: all 40 read no advance, every run; - a 2 ms sleep straight after 8 ms of CPU: all 40 read the burn before them, every run, 40 to 48 mJ for a sleep. Each of the measurement's two reads now sleeps (50 us, then 200 us, 1 ms, 5 ms) until the measuring thread's own energy count (proc_pidinfo PROC_PIDTHREADCOUNTS, read only in the one layout known: 88 bytes, two performance levels here) rose across a sleep, and only then reads the process. A read whose thread was not seen to leave gives the measurement no figure, by the path an unreadable counter already takes: the work does not run, measure_marginal returns None and the caller keeps its estimate. The process counter is Unavailable where the thread's count does not read (proof --measure says so), and the liveness probe still samples with no sleep. The rule and the layout decoding are pure and compiled on every platform. The measurement was already one window around a batch: a second of calls on every spare core, two reads in all. Its workers have exited and been joined before the closing read, and a thread's exit puts its last time on a core into the count before the join returns (measured: a 5 ms burn read the same at the join whether or not the thread slept first), so only the measuring thread needed taking off its core. With it, on that host: quiet, every window read 98% or more of the tests' window and no sleep more than 1% of the window that held the burn; under 40 busy loops (110 runs), 82% or more and 4% or less. A read costs about 80 us quiet and 190 us at the median under the busy loops, against under 1 us. process_window_read_nj is a doc-hidden hook, like the crate's other test hooks: tests/process_energy_short_span.rs, the test that fails without the check, needs a process of its own. The published conformance vectors are unchanged (gen-vectors into a scratch directory is byte-identical to conformance/v1). Co-Authored-By: Claude Opus 5.5 <[email protected]>
eoc-meter: the process counter reads its own count only once the reading thread has left its core
The kernel adds a thread's CPU energy to its process's count (proc_pid_rusage ri_energy_nj) only when the thread leaves a core, and a process's read of its own count does not take the reading thread off its core. MacosProcessEnergyCounter read the count as it stood, so a span bracketed by two reads held up to a scheduling quantum of what the thread spent before the span and lacked the span's own tail, and every consumer here tags a positive delta of the counter Measured. Measured on an M3 Ultra (macOS 27.0.1, load average about 14) against the tests' own reader, which sleeps before each read until the process's context-switch count rose; three runs of 40 spans of each kind: - 1 ms of the reading thread's CPU between two reads: 39, 40 and 40 spans read nothing or under two thirds of the work; - a 2 ms sleep between two reads, straight after 8 ms of CPU: 40, 40 and 37 spans read the burn before them, about 41 mJ for a sleep. read_microjoules now sleeps (50 us, then 200 us, 1 ms, 5 ms) until the reading thread's own energy count (proc_pidinfo PROC_PIDTHREADCOUNTS, read only in the one layout known: 88 bytes, two performance levels here) rose across a sleep, and only then reads the process. A read whose thread was not seen to leave is an error, not a short count: eoc-cascade, eoc-agent and eoc-local all take a failed read as no span and keep their estimate. process_energy_status is Unavailable, with no probe run, where the thread's count does not read, and the liveness probe still samples with no sleep. The rule and the layout decoding are pure and compiled on every platform. With it, on that host, quiet and under 40 busy loops (110 runs): every 1 ms span read 79% or more of the tests' window around it (98% when quiet) and no sleep read more than 3% of the window that held the burn. A read costs 83 us at the median on the quiet host and 226 us under the busy loops, against under 1 us; detect_in_process's docs say so, and that a batch of short operations is bracketed by one span, not one each. tests/process_energy_short_span.rs is the test that fails without it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
jcp-energy: the per-process rung reads its own count only once the metering thread has left its core
SPEC §5.1 rung 3 already says it: the kernel adds a thread's energy to its process's count (proc_pid_rusage ri_energy_nj) only when the thread leaves a core, a process's read of its own count does not take the reading thread off its core, and an implementation that relies on the thread having left MUST check that it did. The reference meter did not. Each end of a span was read from a thread still on its core, so a span held up to a scheduling quantum of what the thread spent before it and lacked its own tail, and was graded HwShunt. Measured on an idle M3 Ultra (macOS 27.0.1) against the tests' own reader, which sleeps before each read until the process's context-switch count rose; 40 spans of each kind a run, six runs: - 1 ms of the metering thread's CPU: all 40 showed no rise and were the default-power estimate, every run; - a 2 ms sleep straight after 8 ms of CPU: 39 or 40 were graded HwShunt on the burn before them, about 41 mJ for a sleep. Now ProcessEnergy::read, the read at each end of a Meter span and of a FabricMeter span, sleeps (50 us, then 200 us, 1 ms, 5 ms) until the metering thread's own energy count (proc_pidinfo PROC_PIDTHREADCOUNTS, decoded only in the one layout known: 88 bytes, two performance levels here) rose across a sleep, and only then reads the process. An end whose thread was not seen to leave is not read: the Meter span is the default-power estimate (process_span's existing path, Estimator, no measured figure) and the FabricMeter span reports no CPU figure. The rung opens only where the thread's count reads. The liveness probe still samples with no sleep. The layout decoding and the rule are pure and compiled on every platform. With it, on that host, idle and under 40 busy loops (110 runs): every span of both kinds was read, the work at 84% or more of the tests' own window and the sleep at 6% or less of the window that held the burn. A read costs about 80 us idle and 169 us at the median under the busy loops, against under 1 us. Co-Authored-By: Claude Opus 5.5 <[email protected]>
jouleclaw-energy: a process reads itself only once the reading thread has left its core
The kernel adds a thread's CPU energy to its process's count (proc_pid_rusage ri_energy_nj) only when the thread leaves a core. A process reading its own count does not take the reading thread off its core, so the read is short of what that thread spent since it last left: a span's first read put up to a scheduling quantum of energy from before the span inside it, and its last read left the span's tail out. Both were sealed HwShunt. Measured against a reader that sleeps before its own reads and checks the process's context-switch count rose, 40 spans of each kind a run, three runs a host (macOS 27.0.1): - 1 ms of the reading thread's CPU: on an M5 Max at a load average of about 90, 25, 31 and 27 spans read no advance or under three quarters of the work; on an idle M3 Ultra, all 40 read no advance, every run; - a 2 ms sleep straight after 8 ms of CPU: on the M5 Max 25, 26 and 31 spans read the CPU before them as the sleep's energy, up to 18 mJ; on the M3 Ultra all 40 read the whole burn, 42 to 53 mJ for a sleep. ProcessCpuCounter now sleeps before each read of its own process (50 us, then 200 us, 1 ms, 5 ms) and reads only once the reading thread's own energy count (proc_pidinfo PROC_PIDTHREADCOUNTS, layout-gated: 88 bytes and two performance levels on both hosts) rose across a sleep. A read whose thread was not seen to leave is EnergyError::NotFolded, no reading: measure() gives no figure, read() fails, and a ProcessSpanMeter span falls back to the host meter. The fold is inside process::energy_nj, the one call span.rs makes for the CPU half, so it holds whatever order span.rs takes its readings in. The counter is Unavailable where the thread's count does not read, and the liveness probe still reads with no sleep. With it, all 40 spans of each kind agreed on both hosts: at least 95% of the work on the loaded M5 Max and 97% on the quiet M3 Ultra, at most 0.5% of the burn before a sleep; and under 40 busy loops on the M3 Ultra (220 runs), at least 84% and at most 2% (the oracle's window also holds both readers' own reads, which do not shrink with the work). A read costs about 80 us on the quiet M3 Ultra, about 200 to 330 us at the median under the busy loops, and 81 to 154 us at the median on the M5 Max at a load average of 135, against 1 us. tests/process_cpu_short_span.rs is the test that fails without the fold. Co-Authored-By: Claude Opus 5.5 <[email protected]>