a process reads its own energy only once the reading thread has left its core #229

Merged
dcharlot merged 5 commits from fix/in-process-self-read-fold into main 2026-10-13 02:22:46 -04:00

2026-10-13

Transaction Science
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-13 00:54:51 -04:00

2026-10-12

Transaction Science
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]>
2026-10-12 20:50:53 -04:00
Transaction Science
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]>
2026-10-12 20:50:53 -04:00
Transaction Science
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]>
2026-10-12 20:50:52 -04:00
Transaction Science
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]>
2026-10-12 20:50:52 -04:00