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
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]>
275 lines
14 KiB
Rust
275 lines
14 KiB
Rust
//! `proof --measure`, end to end: when every hardware source declines, the
|
|
//! output names the reasons, nothing in it claims a hardware reading, and the
|
|
//! cascade's own ModelBased figures stand; when a source meters, the output
|
|
//! names the source, its HwShunt figure and its domain, and the sealed receipt
|
|
//! is unchanged. Every check is against the decision the `proof` process
|
|
//! printed, never a second probe in this process: two independent ~1 s probes
|
|
//! of a borderline counter could disagree.
|
|
//!
|
|
//! The printed statuses are themselves checked against what the process did
|
|
//! with them. Each reason the CLI gives comes from the status it acted on, so
|
|
//! every reason in the output must be the one for its printed status, the
|
|
//! source must be the first live one in order, and a declined meter must
|
|
//! print every source's reason: a CLI that prints one status and acts on
|
|
//! another fails here, whichever status either one is.
|
|
//!
|
|
//! One check is against the hardware: on macOS this test's own process reads
|
|
//! its own CPU energy counter (its own binding of `proc_pid_rusage`) while it
|
|
//! burns CPU. Where that counter keeps moving, the CLI must have metered with
|
|
//! its process counter and printed a non-zero HwShunt figure.
|
|
|
|
use std::process::Command;
|
|
|
|
/// The value after `key=` on the first line that has it, up to the next space.
|
|
fn field<'a>(stdout: &'a str, key: &str) -> Option<&'a str> {
|
|
let at = stdout.find(key)? + key.len();
|
|
stdout[at..].split_whitespace().next()
|
|
}
|
|
|
|
/// Every `IoReportStatus` the CLI can print, each with the reason it gives
|
|
/// when that status is why IOReport is not used (`Live`'s is for a bracket
|
|
/// that returned nothing). Each reason names exactly one status.
|
|
const IOREPORT_REASONS: [(&str, &str); 4] = [
|
|
("CpuRailFrozen", "its CPU energy rail is frozen"),
|
|
("CpuRailAbsent", "has no CPU energy rail"),
|
|
("Unavailable", "could not be opened, or opened but could not be sampled"),
|
|
("Live", "CPU energy rail is live, but the bracket returned no reading"),
|
|
];
|
|
|
|
/// Every process-counter `CounterStatus` the CLI can print, each with its
|
|
/// reason on macOS.
|
|
const PROCESS_REASONS: [(&str, &str); 4] = [
|
|
("Unavailable", "CPU energy counter (proc_pid_rusage, ri_energy_nj) could not be read"),
|
|
("Absent", "CPU energy counter is absent"),
|
|
("Frozen", "CPU energy counter did not keep advancing"),
|
|
("Live", "CPU energy counter is live, but the measurement returned no reading"),
|
|
];
|
|
|
|
/// Every RAPL `CounterStatus` the CLI can print, each with the reason it
|
|
/// gives on Linux when that status is why there is no hardware reading.
|
|
const RAPL_REASONS: [(&str, &str); 4] = [
|
|
("Unavailable", "no readable RAPL counter"),
|
|
("Absent", "none of them is a package zone"),
|
|
("Frozen", "a RAPL package counter is readable but did not keep advancing"),
|
|
("Live", "RAPL's package counters are live, but the bracket returned no reading"),
|
|
];
|
|
|
|
/// Every reason of `table` in the output must be its printed status's own.
|
|
fn reasons_match(stdout: &str, table: &[(&str, &str)], printed: &str, what: &str) {
|
|
for (reason_status, reason) in table {
|
|
if stdout.contains(reason) {
|
|
assert_eq!(
|
|
*reason_status, printed,
|
|
"the CLI printed {what}={printed} but gave the reason for {reason_status}:\n{stdout}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The reason `table` gives for `printed`.
|
|
fn reason_of<'a>(table: &[(&'a str, &'a str)], printed: &str) -> &'a str {
|
|
table.iter().find(|(s, _)| *s == printed).map(|(_, r)| *r).expect("a known status")
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
mod oracle {
|
|
use std::os::raw::c_int;
|
|
use std::time::{Duration, Instant};
|
|
unsafe extern "C" {
|
|
fn proc_pid_rusage(pid: c_int, flavor: c_int, buffer: *mut [u64; 58]) -> c_int;
|
|
fn getpid() -> c_int;
|
|
}
|
|
|
|
/// `ri_energy_nj` of `proc_pid_rusage(getpid(), 6)`, read at byte 336.
|
|
fn energy_nj() -> Option<u64> {
|
|
let mut buf = [0u64; 58];
|
|
// SAFETY: buf is 464 writable bytes, the size of rusage_info_v6.
|
|
(unsafe { proc_pid_rusage(getpid(), 6, &mut buf) } == 0).then_some(buf[42])
|
|
}
|
|
|
|
/// This test process's own counter, read every ~25 ms for 600 ms while
|
|
/// this thread burns CPU between reads: (reads, reads at which it had
|
|
/// changed), or `None` if it cannot be read.
|
|
pub fn watch() -> Option<(u32, u32)> {
|
|
let mut prev = energy_nj()?;
|
|
let (mut reads, mut changes) = (0u32, 0u32);
|
|
let start = Instant::now();
|
|
while start.elapsed() < Duration::from_millis(600) {
|
|
let t = Instant::now();
|
|
let mut acc = 0u64;
|
|
while t.elapsed() < Duration::from_millis(25) {
|
|
acc = std::hint::black_box(acc.wrapping_add(1));
|
|
}
|
|
let now = energy_nj()?;
|
|
reads += 1;
|
|
changes += u32::from(now != prev);
|
|
prev = now;
|
|
}
|
|
Some((reads, changes))
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn measure_names_its_source_or_why_it_declined_and_leaves_the_receipt_alone() {
|
|
let out = Command::new(env!("CARGO_BIN_EXE_proof"))
|
|
.arg("--measure")
|
|
.output()
|
|
.expect("the proof binary runs");
|
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
assert!(
|
|
out.status.success(),
|
|
"proof --measure exited {:?}\n{stdout}\n{}",
|
|
out.status,
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
let available = match field(&stdout, "--measure: hardware meter available=") {
|
|
Some("true") => true,
|
|
Some("false") => false,
|
|
other => panic!("proof --measure did not print its meter decision ({other:?}):\n{stdout}"),
|
|
};
|
|
let printed = |key: &str, table: &[(&str, &str)]| {
|
|
let v = field(&stdout, key).unwrap_or_else(|| panic!("proof --measure did not print {key}:\n{stdout}"));
|
|
assert!(table.iter().any(|&(s, _)| s == v), "proof --measure printed an unknown {key}{v:?}:\n{stdout}");
|
|
v.to_string()
|
|
};
|
|
let status = printed("ioreport_status=", &IOREPORT_REASONS);
|
|
let process = printed("process_energy_status=", &PROCESS_REASONS);
|
|
let rapl = printed("rapl_status=", &RAPL_REASONS);
|
|
let source = field(&stdout, " · source=").unwrap_or_else(|| panic!("no source printed:\n{stdout}")).to_string();
|
|
let declined = stdout.contains("--measure: no hardware energy reading on this host: ");
|
|
// Recorded so a log says which branch below was actually asserted.
|
|
eprintln!(
|
|
"proof --measure: declined={declined} available={available} source={source} process_energy_status={process} \
|
|
ioreport_status={status} rapl_status={rapl}"
|
|
);
|
|
if let Some(line) = stdout.lines().find(|l| l.trim_start().starts_with("measured: ")) {
|
|
eprintln!("proof --measure: {}", line.trim());
|
|
}
|
|
|
|
// The sealed receipt is the cascade's own figure for the FIRST goal, on
|
|
// every branch: the first row's figure, ModelBased, never the --measure
|
|
// figure.
|
|
let first_row_uj: u64 = stdout
|
|
.lines()
|
|
.find(|l| l.trim_start().starts_with("T0:automation"))
|
|
.and_then(|l| l.split_whitespace().nth(1))
|
|
.and_then(|n| n.parse().ok())
|
|
.unwrap_or_else(|| panic!("no first goal row:\n{stdout}"));
|
|
let receipt = stdout
|
|
.lines()
|
|
.find(|l| l.trim_start().starts_with("receipt: closed at"))
|
|
.unwrap_or_else(|| panic!("no sealed receipt in the output:\n{stdout}"));
|
|
let receipt_uj: u64 = receipt
|
|
.split(" · ")
|
|
.nth(1)
|
|
.and_then(|f| f.strip_suffix(" uj"))
|
|
.and_then(|n| n.parse().ok())
|
|
.unwrap_or_else(|| panic!("no uj figure in the receipt line: {receipt}"));
|
|
assert!(receipt_uj > 0, "the receipt carries a zero figure for a proof that ran: {receipt}");
|
|
assert_eq!(receipt_uj, first_row_uj, "the receipt is not the first goal's own figure: {receipt}\n{stdout}");
|
|
assert!(receipt.contains("provenance ModelBased"), "the receipt's provenance changed: {receipt}");
|
|
assert!(receipt.contains("closed at T0:automation"), "the receipt is not the first goal's: {receipt}");
|
|
|
|
assert_eq!(declined, !available, "the CLI's output disagrees with the decision it printed:\n{stdout}");
|
|
assert_eq!(available, source != "none", "available={available} but source={source}:\n{stdout}");
|
|
|
|
// RAPL is a Linux interface: elsewhere it is Unavailable and no RAPL
|
|
// reason is ever given. On Linux the meter is available exactly when RAPL
|
|
// is Live, and a declined meter gives the printed RAPL status's reason.
|
|
if cfg!(target_os = "linux") {
|
|
assert_eq!(source, if rapl == "Live" { "Rapl" } else { "none" }, "on Linux the source is RAPL exactly when Live:\n{stdout}");
|
|
reasons_match(&stdout, &RAPL_REASONS, &rapl, "rapl_status");
|
|
if declined {
|
|
assert!(stdout.contains(reason_of(&RAPL_REASONS, &rapl)), "declined without RAPL's reason:\n{stdout}");
|
|
}
|
|
} else {
|
|
assert_eq!(rapl, "Unavailable", "a RAPL status other than Unavailable off Linux:\n{stdout}");
|
|
for (reason_status, reason) in RAPL_REASONS {
|
|
assert!(!stdout.contains(reason), "a RAPL reason ({reason_status}) off Linux:\n{stdout}");
|
|
}
|
|
}
|
|
if cfg!(target_os = "macos") {
|
|
// The first live source, in order: the process counter, then IOReport.
|
|
let want = if process == "Live" {
|
|
"ProcessEnergy"
|
|
} else if status == "Live" {
|
|
"IoReport"
|
|
} else {
|
|
"none"
|
|
};
|
|
assert_eq!(source, want, "process_energy_status={process} ioreport_status={status}, yet source={source}:\n{stdout}");
|
|
reasons_match(&stdout, &IOREPORT_REASONS, &status, "ioreport_status");
|
|
reasons_match(&stdout, &PROCESS_REASONS, &process, "process_energy_status");
|
|
if declined {
|
|
assert!(stdout.contains(reason_of(&IOREPORT_REASONS, &status)), "declined without IOReport's reason:\n{stdout}");
|
|
assert!(stdout.contains(reason_of(&PROCESS_REASONS, &process)), "declined without the process counter's reason:\n{stdout}");
|
|
}
|
|
if source == "IoReport" {
|
|
assert!(
|
|
stdout.contains("CPU energy counter not used: ") && stdout.contains(reason_of(&PROCESS_REASONS, &process)),
|
|
"IOReport metered, but the CLI did not say why the process counter was not used:\n{stdout}"
|
|
);
|
|
}
|
|
// Against the hardware: this process's own counter.
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let seen = oracle::watch();
|
|
eprintln!("proof --measure: this test process's own counter (reads, changes) = {seen:?}");
|
|
if let Some((reads, changes)) = seen.filter(|&(r, c)| c >= 5 && c * 5 >= r * 4) {
|
|
assert_eq!(
|
|
source, "ProcessEnergy",
|
|
"this test's own process counter changed at {changes} of {reads} reads, yet the CLI metered with \
|
|
{source}:\n{stdout}"
|
|
);
|
|
} else {
|
|
assert_ne!(source, "ProcessEnergy", "the test's own counter did not keep moving ({seen:?}), yet:\n{stdout}");
|
|
}
|
|
}
|
|
} else {
|
|
// Off macOS there is no IOReport and no process counter: both are
|
|
// Unavailable, and neither reason is ever given.
|
|
assert_eq!(status, "Unavailable", "an IOReport status other than Unavailable off macOS:\n{stdout}");
|
|
assert_eq!(process, "Unavailable", "a process counter off macOS:\n{stdout}");
|
|
for (reason_status, reason) in IOREPORT_REASONS.iter().chain(&PROCESS_REASONS) {
|
|
assert!(!stdout.contains(reason), "an IOReport or process-counter reason ({reason_status}) off macOS:\n{stdout}");
|
|
}
|
|
}
|
|
|
|
if declined {
|
|
assert!(!stdout.contains("HwShunt"), "the meter declined, yet the output claims a hardware reading:\n{stdout}");
|
|
assert!(!stdout.contains("idle noise"), "the meter declined, yet the output blames a noisy machine:\n{stdout}");
|
|
assert!(!stdout.contains("--measure: metering a real"), "the meter declined, yet the CLI metered:\n{stdout}");
|
|
assert!(
|
|
stdout.contains("The figures above stay ModelBased"),
|
|
"a declined meter must leave the ModelBased figures standing:\n{stdout}"
|
|
);
|
|
} else {
|
|
assert!(
|
|
stdout.contains("--measure: metering a real 16-variable T0 proof") && stdout.contains(" measured: "),
|
|
"neither a declined meter nor a measurement in the output:\n{stdout}"
|
|
);
|
|
}
|
|
if source == "ProcessEnergy" {
|
|
let measured = stdout
|
|
.lines()
|
|
.find(|l| l.trim_start().starts_with("measured: "))
|
|
.unwrap_or_else(|| panic!("the process counter was selected, yet no measured line:\n{stdout}"));
|
|
let uj: u64 = measured
|
|
.trim_start()
|
|
.strip_prefix("measured: ")
|
|
.and_then(|r| r.split_whitespace().next())
|
|
.and_then(|n| n.parse().ok())
|
|
.unwrap_or_else(|| panic!("no figure in: {measured}"));
|
|
assert!(uj > 0, "the process counter metered a 2^16-row proof as {uj} uj: {measured}");
|
|
assert!(
|
|
measured.contains("provenance HwShunt") && measured.contains("source ProcessEnergy"),
|
|
"the process counter's figure is not graded HwShunt and named by its source: {measured}"
|
|
);
|
|
assert!(
|
|
stdout.contains("domain: this process's CPU energy") && stdout.contains("not other processes"),
|
|
"the process counter's figure was printed without its domain:\n{stdout}"
|
|
);
|
|
assert!(!stdout.contains("idle noise"), "the process counter has no idle baseline to blame:\n{stdout}");
|
|
assert_ne!(uj, receipt_uj, "the --measure figure leaked into the receipt:\n{stdout}");
|
|
}
|
|
}
|