proof-cascade: measure CPU work with the per-process energy counter, which macOS 27 keeps live #120
8 changed files with 1499 additions and 306 deletions
|
|
@ -37,10 +37,10 @@ proof/
|
|||
|
||||
- **`proof-schema`** — the content-addressed goal (`blake3(statement‖0x00‖context)`), the cheapest-first tier ladder, the honest energy model (`HwShunt`/`ModelBased`/`Estimator` with the coupling enforced), `SpecGrounding`, the verdict, and the `Solver` trait every backend implements. Canonical det-CBOR content addressing.
|
||||
- **`proof-solvers`** — two **real** rungs, their energy modeled from measured wall time (`ModelBased`, a ~15 W power model — not a hardware reading): a propositional decider (T0) that parses a boolean goal and decides validity by truth-table in process, and `z3` (T1) — shell or kept **resident** across goals — that decides an SMT-LIB goal (assert the negation; `unsat` ⇒ valid). Each refuses rather than fakes, so the router escalates. The model rungs (T2/T3) are honest stand-ins behind the same trait.
|
||||
- **`proof-cascade`** — the escalation router (every joule accounted, `wasted_uj` disclosed), the content-addressed proof cache (the energy flywheel, durable as line-delimited JSON), batch energy accounting against a frontier-only baseline with the per-tier Amdahl breakdown, and a **real hardware energy meter** (`meter` — the `HwShunt` backend): it brackets a real proof, run on every core, against an idle baseline using the platform's energy interface (IOReport's Energy Model, read directly through its C API on Apple Silicon and used only while every CPU energy rail is live, one per die on a multi-die part; RAPL's package counters on Linux, used only while every one is live) and returns measured microjoules, so the headline `joules per verified proof` can be a real reading, not only a model. A vendor counter's reading is graded `HwShunt` only while the counter is live for the domain it claims ([`SPEC.md`](SPEC.md) §5), and the meter checks that before it brackets anything, by one rule on both platforms (the counter keeps advancing in at least 5 sample intervals, at least 4 of every 5, over at least 300 ms while the probe burns CPU): on Apple Silicon for every IOReport CPU energy rail the figure sums (`CPU Energy`, or each die's `DIE_<n>_CPU Energy`), on Linux for every RAPL package counter the figure sums (`psys` is neither summed nor probed). Returns nothing where no interface is reachable or its counter is not live — the caller keeps the honestly-tagged `ModelBased` figure rather than inventing one.
|
||||
- **`proof-cascade`** — the escalation router (every joule accounted, `wasted_uj` disclosed), the content-addressed proof cache (the energy flywheel, durable as line-delimited JSON), batch energy accounting against a frontier-only baseline with the per-tier Amdahl breakdown, and a **real hardware energy meter** (`meter` — the `HwShunt` backend): it runs a real proof on every spare core and returns its measured microjoules from the first live source — on macOS **this process's own CPU energy counter** (`proc_pid_rusage`'s `ri_energy_nj`, public API, no privilege), read before the workers start and after they are joined, so other processes are excluded by construction and no idle baseline is subtracted; else IOReport's Energy Model, read directly through its C API on Apple Silicon and bracketed against idle; on Linux RAPL's package counters, bracketed against idle — so the headline `joules per verified proof` can be a real reading, not only a model. A vendor or kernel counter's reading is graded `HwShunt` only while the counter is live for the domain it claims ([`SPEC.md`](SPEC.md) §5), and the meter checks that before it meters anything, by one rule for every source (the counter keeps advancing in at least 5 sample intervals, at least 4 of every 5, over at least 300 ms while the probe burns CPU): the process counter itself, every IOReport CPU energy rail the figure sums (`CPU Energy`, or each die's `DIE_<n>_CPU Energy`), every RAPL package counter the figure sums (`psys` is neither summed nor probed). Each figure carries its domain: the process counter's is this process's CPU energy only — not other processes, child processes, GPU, ANE or DRAM. Returns nothing where no source is live — the caller keeps the honestly-tagged `ModelBased` figure rather than inventing one.
|
||||
- **`proof-receipt`** — the `proof/1` JCR-1 receipt: canonical det-CBOR + COSE_Sign1/EdDSA, vendored byte-compatible with the family's cross-language verifiers; refuses to seal an unproved goal.
|
||||
- **`proof-conformance`** — C1–C5 + a negative pack, all checks real, published vectors in `conformance/v1/`.
|
||||
- **`proof-cli`** — the `proof` reference demo: a proof workload reporting joules-per-proof against a frontier-only baseline — like for like, charged only for the goals the cascade proved, with the all-goals figure and its goal count beside it — the Amdahl breakdown, the cache flywheel, and a sealed + verified receipt. `proof --measure` adds a **real hardware reading** of a non-trivial T0 proof (`HwShunt`) where the meter produces one; that needs a counter the meter has seen to be live (on Apple Silicon every IOReport CPU energy rail, on Linux every RAPL package counter). On the Macs measured so far (macOS 27.0.1, an M5 Max and an M3 Ultra) IOReport's CPU energy rail is frozen for an unprivileged reader, so `--measure` declines there, says why, and leaves every figure `ModelBased`; on Linux, RAPL's `energy_uj` is root-only on current kernels.
|
||||
- **`proof-cli`** — the `proof` reference demo: a proof workload reporting joules-per-proof against a frontier-only baseline — like for like, charged only for the goals the cascade proved, with the all-goals figure and its goal count beside it — the Amdahl breakdown, the cache flywheel, and a sealed + verified receipt. `proof --measure` adds a **real hardware reading** of a non-trivial T0 proof (`HwShunt`) where the meter produces one, and prints which source metered it, its domain, and why each other source was not used; that needs a counter the meter has seen to be live. On the Macs measured so far (macOS 27.0.1, an M5 Max and an M3 Ultra) IOReport's CPU energy rail is frozen for an unprivileged reader, but this process's CPU energy counter is live, so `--measure` meters with it; the receipt still seals the cascade's own `ModelBased` figure for the first goal. On Linux, RAPL's `energy_uj` is root-only on current kernels.
|
||||
|
||||
`cargo test --workspace` green; `cargo clippy --workspace --all-targets` is
|
||||
warning-free for Linux, and on macOS reports two lints in proof-cascade's
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,19 +1,21 @@
|
|||
//! The IOReport liveness probe behind `ioreport_status()` runs once per
|
||||
//! PROCESS, however many threads ask at once, through the status, the
|
||||
//! selection and the metering entry point: a probe per call or per thread
|
||||
//! would burn ~1 s on each caller's thread (inside `measure_marginal_uj`'s
|
||||
//! bracket, a whole probe in the busy window) and could flip the verdict
|
||||
//! mid-process. (`power_over` is private; the lib test
|
||||
//! The liveness probes behind `ioreport_status()` and
|
||||
//! `process_energy_status()` each run once per PROCESS, however many threads
|
||||
//! ask at once, through the statuses, the selection and the metering entry
|
||||
//! point: a probe per call or per thread would burn up to ~1 s on each
|
||||
//! caller's thread (inside a measurement, a whole probe in its window) and
|
||||
//! could flip the verdict mid-process. (`power_over` is private; the lib test
|
||||
//! `the_gated_entry_points_read_the_cached_status` pins it the same way, on
|
||||
//! the calling thread's CPU.)
|
||||
//!
|
||||
//! This file holds this one test, so that it runs in a process of its own and
|
||||
//! IS the first use of the meter in that process. The counter it reads,
|
||||
//! `real_probe_runs()`, counts runs of the production probe (`host_status`,
|
||||
//! whose first statement it is), not lookups of the cache in front of it.
|
||||
//! IS the first use of the meter in that process. The counters it reads,
|
||||
//! `real_probe_runs()` and `real_process_probe_runs()`, count runs of the
|
||||
//! production probes (`host_status`, `process_host_status`, whose first
|
||||
//! statements they are), not lookups of the caches in front of them.
|
||||
|
||||
use proof_cascade::meter::{
|
||||
hardware_meter_available, ioreport_status, measure_marginal_uj, real_probe_runs, IoReportStatus,
|
||||
hardware_meter_available, ioreport_status, measure_marginal_uj, meter_source, process_energy_status,
|
||||
real_probe_runs, real_process_probe_runs, CounterStatus, IoReportStatus, MeterSource,
|
||||
};
|
||||
use std::sync::{Arc, Barrier};
|
||||
use std::time::Duration;
|
||||
|
|
@ -46,17 +48,18 @@ fn thread_cpu() -> Duration {
|
|||
Duration::ZERO
|
||||
}
|
||||
|
||||
/// Eight threads, released together, each ask for the status and then select
|
||||
/// the meter the way a caller does (`hardware_meter_available()` is the
|
||||
/// selection; `measure_marginal_uj` selects and, declined, gives no figure).
|
||||
/// The probe must have run exactly once for all of them. Once a thread has its
|
||||
/// status, its selection calls must be warm: together they may not burn 10 ms
|
||||
/// of that thread's CPU, where re-running the probe by any route costs at
|
||||
/// least six reads of the Energy Model group (~7 ms of thread CPU each) and
|
||||
/// five 25 ms ticks, and normally ~1 s of burn.
|
||||
/// Eight threads, released together, each ask for both statuses and then
|
||||
/// select the meter the way a caller does (`meter_source()` and
|
||||
/// `hardware_meter_available()` are the selection; `measure_marginal_uj`
|
||||
/// selects and, declined, gives no figure). Each probe must have run exactly
|
||||
/// once for all of them. Once a thread has its statuses, its selection calls
|
||||
/// must be warm: together they may not burn 10 ms of that thread's CPU, where
|
||||
/// re-running a probe by any route costs at least five 25 ms ticks, and
|
||||
/// normally 0.3–1 s of burn.
|
||||
#[test]
|
||||
fn the_liveness_probe_runs_once_per_process() {
|
||||
assert_eq!(real_probe_runs(), 0, "the meter was used in this process before this test: this is not its first use");
|
||||
assert_eq!(real_process_probe_runs(), 0, "the process counter was used in this process before this test");
|
||||
const THREADS: usize = 8;
|
||||
let gate = Arc::new(Barrier::new(THREADS));
|
||||
let handles: Vec<_> = (0..THREADS)
|
||||
|
|
@ -65,42 +68,54 @@ fn the_liveness_probe_runs_once_per_process() {
|
|||
std::thread::spawn(move || {
|
||||
gate.wait();
|
||||
let status = ioreport_status();
|
||||
let process = process_energy_status();
|
||||
let c0 = thread_cpu();
|
||||
let source = meter_source();
|
||||
let selected = hardware_meter_available();
|
||||
let again = ioreport_status();
|
||||
let again = (ioreport_status(), process_energy_status());
|
||||
let figure = if selected { None } else { measure_marginal_uj(cpu_work) };
|
||||
let warm = thread_cpu() - c0;
|
||||
(status, again, selected, figure, warm)
|
||||
(status, process, source, again, selected, figure, warm)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let seen: Vec<_> = handles.into_iter().map(|h| h.join().expect("a probing thread panicked")).collect();
|
||||
let runs = real_probe_runs();
|
||||
let warmest = seen.iter().map(|s| s.4).max().unwrap_or_default();
|
||||
let (runs, process_runs) = (real_probe_runs(), real_process_probe_runs());
|
||||
let warmest = seen.iter().map(|s| s.6).max().unwrap_or_default();
|
||||
let (status, process, source) = (seen[0].0, seen[0].1, seen[0].2);
|
||||
eprintln!(
|
||||
"ioreport cache: {THREADS} threads saw (status={:?}, selected={}); real probe runs in this process: {runs}; \
|
||||
warm selection calls burned at most {warmest:?} of a thread's CPU",
|
||||
seen[0].0, seen[0].2
|
||||
"ioreport cache: {THREADS} threads saw (ioreport={status:?}, process={process:?}, source={source:?}); real \
|
||||
probe runs in this process: ioreport {runs}, process {process_runs}; warm selection calls burned at most \
|
||||
{warmest:?} of a thread's CPU"
|
||||
);
|
||||
for (i, (status, again, selected, figure, _)) in seen.iter().enumerate() {
|
||||
assert_eq!((*status, *again), (seen[0].0, seen[0].0), "thread {i} was given a different status");
|
||||
assert_eq!(*selected, seen[0].2, "thread {i} made a different selection");
|
||||
for (i, (s, p, src, again, selected, figure, _)) in seen.iter().enumerate() {
|
||||
assert_eq!((*s, *p, *again), (status, process, (status, process)), "thread {i} was given a different status");
|
||||
assert_eq!((*src, *selected), (source, source.is_some()), "thread {i} made a different selection");
|
||||
if !selected {
|
||||
assert_eq!(*figure, None, "thread {i}: a declined meter gave a figure");
|
||||
}
|
||||
}
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(seen[0].2, seen[0].0 == IoReportStatus::Live, "on macOS the meter is selected exactly when Live");
|
||||
assert_eq!(runs, 1, "the liveness probe ran {runs} times in this process ({THREADS} threads asked at once)");
|
||||
let want = if process == CounterStatus::Live {
|
||||
Some(MeterSource::ProcessEnergy)
|
||||
} else if status == IoReportStatus::Live {
|
||||
Some(MeterSource::IoReport)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
assert_eq!(source, want, "on macOS: the process counter if Live, else IOReport if Live, else none");
|
||||
assert_eq!(runs, 1, "the IOReport probe ran {runs} times in this process ({THREADS} threads asked at once)");
|
||||
assert_eq!(process_runs, 1, "the process-counter probe ran {process_runs} times ({THREADS} threads asked at once)");
|
||||
for (i, s) in seen.iter().enumerate() {
|
||||
assert!(
|
||||
s.4 < Duration::from_millis(10),
|
||||
s.6 < Duration::from_millis(10),
|
||||
"thread {i}: its warm selection calls burned {:?} of its CPU: something probed again, uncached",
|
||||
s.4
|
||||
s.6
|
||||
);
|
||||
}
|
||||
} else {
|
||||
assert_eq!(seen[0].0, IoReportStatus::Unavailable, "IOReport off macOS");
|
||||
assert_eq!(runs, 0, "a liveness probe ran off macOS");
|
||||
assert_eq!(status, IoReportStatus::Unavailable, "IOReport off macOS");
|
||||
assert_eq!(process, CounterStatus::Unavailable, "a process counter off macOS");
|
||||
assert_eq!((runs, process_runs), (0, 0), "a liveness probe ran off macOS");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
//! The unopenable path of the whole production chain — probe, status, cache,
|
||||
//! selection, metering — on any host. Neither Mac that runs these tests lacks
|
||||
//! an Energy Model group, so `simulate_no_energy_model_group()` makes this
|
||||
//! process's probe find none before the meter's first use. This file holds
|
||||
//! this one test so that it runs in a process of its own and the hook cannot
|
||||
//! reach any other test.
|
||||
//! process's probe find none before the meter's first use. The process CPU
|
||||
//! energy counter, which ranks above IOReport and is live on those Macs, is
|
||||
//! held frozen (`simulate_frozen_process_energy()`), so that every source
|
||||
//! declines and "no meter at all" is reachable. This file holds this one test
|
||||
//! so that it runs in a process of its own and the hooks cannot reach any
|
||||
//! other test.
|
||||
|
||||
use proof_cascade::meter::{
|
||||
hardware_meter_available, ioreport_status, measure_marginal_uj, real_probe_runs, simulate_no_energy_model_group,
|
||||
hardware_meter_available, ioreport_status, measure_marginal_uj, process_energy_status, real_probe_runs,
|
||||
real_process_probe_runs, simulate_frozen_process_energy, simulate_no_energy_model_group, CounterStatus,
|
||||
IoReportStatus,
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
|
@ -21,13 +25,15 @@ fn counted_work() {
|
|||
}
|
||||
|
||||
/// With no Energy Model group to subscribe to, the status is `Unavailable` —
|
||||
/// never `Live`, never `CpuRailAbsent` — for every thread that asks, the meter
|
||||
/// is never selected, metering gives no figure and never runs the work, and the
|
||||
/// probe still ran exactly once.
|
||||
/// never `Live`, never `CpuRailAbsent` — for every thread that asks; with the
|
||||
/// process counter held, it is `Frozen`. The meter is never selected, metering
|
||||
/// gives no figure and never runs the work, and each probe ran exactly once.
|
||||
#[test]
|
||||
fn a_host_with_no_energy_model_group_is_unavailable_and_every_meter_declines() {
|
||||
assert_eq!(real_probe_runs(), 0, "the meter was used in this process before this test: this is not its first use");
|
||||
assert_eq!(real_process_probe_runs(), 0, "the process counter was used in this process before this test");
|
||||
simulate_no_energy_model_group();
|
||||
simulate_frozen_process_energy();
|
||||
const THREADS: usize = 4;
|
||||
let gate = Arc::new(Barrier::new(THREADS));
|
||||
let handles: Vec<_> = (0..THREADS)
|
||||
|
|
@ -36,23 +42,30 @@ fn a_host_with_no_energy_model_group_is_unavailable_and_every_meter_declines() {
|
|||
std::thread::spawn(move || {
|
||||
gate.wait();
|
||||
let status = ioreport_status();
|
||||
let process = process_energy_status();
|
||||
let selected = hardware_meter_available();
|
||||
let figure = if cfg!(target_os = "macos") { measure_marginal_uj(counted_work) } else { None };
|
||||
(status, selected, figure)
|
||||
(status, process, selected, figure)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let seen: Vec<_> = handles.into_iter().map(|h| h.join().expect("a thread panicked")).collect();
|
||||
let runs = real_probe_runs();
|
||||
let process_runs = real_process_probe_runs();
|
||||
let work = WORK_CALLS.load(Ordering::SeqCst);
|
||||
eprintln!("ioreport unopenable: {THREADS} threads saw {seen:?}; real probe runs: {runs}; work calls: {work}");
|
||||
for (i, (status, selected, figure)) in seen.iter().enumerate() {
|
||||
eprintln!(
|
||||
"ioreport unopenable: {THREADS} threads saw {seen:?}; real probe runs: {runs}; process probe runs: \
|
||||
{process_runs}; work calls: {work}"
|
||||
);
|
||||
for (i, (status, process, selected, figure)) in seen.iter().enumerate() {
|
||||
assert_eq!(*status, IoReportStatus::Unavailable, "thread {i}: no Energy Model group, yet {status:?}");
|
||||
if cfg!(target_os = "macos") {
|
||||
assert!(!selected, "thread {i}: the meter was selected with no Energy Model group");
|
||||
assert_eq!(*figure, None, "thread {i}: a figure came out with no Energy Model group");
|
||||
assert_eq!(*process, CounterStatus::Frozen, "thread {i}: a held process counter was called {process:?}");
|
||||
assert!(!selected, "thread {i}: a meter was selected with no Energy Model group and a held counter");
|
||||
assert_eq!(*figure, None, "thread {i}: a figure came out with no Energy Model group and a held counter");
|
||||
}
|
||||
}
|
||||
assert_eq!(work, 0, "the work ran {work} times with no usable meter");
|
||||
assert_eq!(runs, usize::from(cfg!(target_os = "macos")), "the liveness probe's runs in this process");
|
||||
assert_eq!(process_runs, usize::from(cfg!(target_os = "macos")), "the process-counter probe's runs");
|
||||
}
|
||||
|
|
|
|||
210
proof/proof-rs/crates/proof-cascade/tests/process_energy.rs
Normal file
210
proof/proof-rs/crates/proof-cascade/tests/process_energy.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
//! This process's CPU energy counter as a meter, on the real hardware.
|
||||
//!
|
||||
//! Every test here checks the meter against an independent oracle: the tests'
|
||||
//! own binding of `proc_pid_rusage(getpid(), 6)`, read into a raw buffer at
|
||||
//! byte 336, with their own loop and their own literals. The oracle watches
|
||||
//! the counter for one second while a helper thread burns CPU and calls it
|
||||
//! moving only if it changed at no fewer than 5 of its reads and at 80% of
|
||||
//! them. Where the oracle sees the counter move, the meter must have selected
|
||||
//! it and must meter with it; where it does not, the meter must not. No branch
|
||||
//! passes without asserting.
|
||||
//!
|
||||
//! The process counter counts everything this process's threads burn, so the
|
||||
//! tests in this file run one at a time (`SERIAL`), and nothing else here runs
|
||||
//! while one of them meters.
|
||||
|
||||
use proof_cascade::meter::{measure_marginal, meter_source, process_energy_status, CounterStatus, MeterSource};
|
||||
use proof_schema::Provenance;
|
||||
use std::sync::{Mutex, MutexGuard, OnceLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
static SERIAL: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn serial() -> MutexGuard<'static, ()> {
|
||||
SERIAL.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// What the oracle saw of the counter over its window.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Oracle {
|
||||
moved: bool,
|
||||
reads: u32,
|
||||
changes: u32,
|
||||
advanced_nj: u64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod ffi {
|
||||
use std::os::raw::c_int;
|
||||
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(), RUSAGE_INFO_V6 = 6)`: a
|
||||
/// 464-byte buffer, read at byte 336 (its 43rd `uint64_t`).
|
||||
pub 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])
|
||||
}
|
||||
}
|
||||
|
||||
/// The oracle's verdict, taken once for this test binary (under `SERIAL`).
|
||||
fn oracle() -> Oracle {
|
||||
static ORACLE: OnceLock<Oracle> = OnceLock::new();
|
||||
*ORACLE.get_or_init(watch_counter)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn watch_counter() -> Oracle {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
let Some(first) = ffi::energy_nj() else {
|
||||
return Oracle { moved: false, reads: 0, changes: 0, advanced_nj: 0 };
|
||||
};
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let burner = {
|
||||
let stop = stop.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut acc = 0u64;
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
acc = std::hint::black_box(acc.wrapping_add(1));
|
||||
}
|
||||
})
|
||||
};
|
||||
let started = Instant::now();
|
||||
let (mut prev, mut reads, mut changes) = (first, 0u32, 0u32);
|
||||
while started.elapsed() < Duration::from_secs(1) {
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
let now = ffi::energy_nj().expect("the oracle's read failed after its first succeeded");
|
||||
reads += 1;
|
||||
changes += u32::from(now != prev);
|
||||
prev = now;
|
||||
}
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = burner.join();
|
||||
let moved = changes >= 5 && changes * 5 >= reads * 4;
|
||||
Oracle { moved, reads, changes, advanced_nj: prev - first }
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn watch_counter() -> Oracle {
|
||||
Oracle { moved: false, reads: 0, changes: 0, advanced_nj: 0 }
|
||||
}
|
||||
|
||||
/// Real, cheap, pure CPU work: a fixed fold.
|
||||
fn cheap() {
|
||||
std::hint::black_box((0..20_000u64).fold(0u64, |a, b| a.wrapping_mul(31).wrapping_add(b)));
|
||||
}
|
||||
|
||||
/// 2 ms of computing, by the wall clock.
|
||||
fn compute_2ms() {
|
||||
let t = Instant::now();
|
||||
let mut acc = 0u64;
|
||||
while t.elapsed() < Duration::from_millis(2) {
|
||||
acc = std::hint::black_box(acc.wrapping_add(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// 2 ms of sleeping.
|
||||
fn sleep_2ms() {
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
}
|
||||
|
||||
/// The counter is selected exactly when the oracle sees it keep moving: live
|
||||
/// and first in line where it moves, never selected where it does not.
|
||||
#[test]
|
||||
fn the_process_counter_is_live_exactly_when_the_oracle_sees_it_move() {
|
||||
let _serial = serial();
|
||||
let status = process_energy_status();
|
||||
let o = oracle();
|
||||
let source = meter_source();
|
||||
eprintln!("process liveness: status={status:?} source={source:?} oracle={o:?}");
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(
|
||||
status == CounterStatus::Live,
|
||||
o.moved,
|
||||
"the probe says {status:?}; the oracle saw the counter change at {} of {} reads (+{} nJ)",
|
||||
o.changes,
|
||||
o.reads,
|
||||
o.advanced_nj
|
||||
);
|
||||
if o.moved {
|
||||
assert_eq!(source, Some(MeterSource::ProcessEnergy), "a live process counter was not selected first");
|
||||
} else {
|
||||
assert_ne!(source, Some(MeterSource::ProcessEnergy), "a process counter the oracle saw hold was selected");
|
||||
}
|
||||
} else {
|
||||
assert_eq!(status, CounterStatus::Unavailable, "a process counter off macOS");
|
||||
assert_ne!(source, Some(MeterSource::ProcessEnergy));
|
||||
}
|
||||
}
|
||||
|
||||
/// A fixed cheap closure meters as a non-zero `HwShunt` figure from the
|
||||
/// process counter, roughly stable: three calls within a factor of 3.
|
||||
#[test]
|
||||
fn a_fixed_cheap_closure_meters_as_a_stable_hwshunt_figure() {
|
||||
let _serial = serial();
|
||||
if meter_source() == Some(MeterSource::ProcessEnergy) {
|
||||
let figures: Vec<_> = (0..3).map(|_| measure_marginal(cheap).expect("a live process counter gave no figure")).collect();
|
||||
eprintln!("process stability: cheap closure, three calls: {figures:?}");
|
||||
for m in &figures {
|
||||
assert_eq!(m.source, MeterSource::ProcessEnergy, "{m:?}");
|
||||
assert!(m.uj > 0, "a live process counter metered real work as zero: {m:?}");
|
||||
let e = m.energy();
|
||||
assert_eq!((e.measured_uj, e.provenance), (Some(m.uj), Provenance::HwShunt), "{m:?}");
|
||||
assert!(e.check_provenance().is_ok(), "{e:?}");
|
||||
}
|
||||
let (lo, hi) = (figures.iter().map(|m| m.uj).min().unwrap(), figures.iter().map(|m| m.uj).max().unwrap());
|
||||
assert!(hi <= 3 * lo, "three calls on the same closure spread from {lo} to {hi} uJ, more than 3x");
|
||||
} else {
|
||||
let o = oracle();
|
||||
assert!(!o.moved, "the oracle saw the process counter move ({o:?}), yet it was not selected");
|
||||
let m = measure_marginal(cheap);
|
||||
assert_ne!(m.map(|m| m.source), Some(MeterSource::ProcessEnergy), "a process-counter figure from a counter not selected");
|
||||
}
|
||||
}
|
||||
|
||||
/// Sleeping is not computing: a closure that sleeps 2 ms meters at under a
|
||||
/// tenth of one that computes for 2 ms — and stays there while four busy
|
||||
/// child processes burn CPU beside it, which the counter excludes.
|
||||
#[test]
|
||||
fn a_closure_that_sleeps_meters_far_less_than_one_that_computes() {
|
||||
let _serial = serial();
|
||||
if meter_source() == Some(MeterSource::ProcessEnergy) {
|
||||
let compute = measure_marginal(compute_2ms).expect("computing gave no figure");
|
||||
let sleep = measure_marginal(sleep_2ms).expect("sleeping gave no figure");
|
||||
let busy: Vec<_> = (0..4)
|
||||
.map(|_| {
|
||||
std::process::Command::new("/bin/sh")
|
||||
.args(["-c", "while :; do :; done"])
|
||||
.spawn()
|
||||
.expect("a busy child process starts")
|
||||
})
|
||||
.collect();
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let beside_children = measure_marginal(sleep_2ms);
|
||||
for mut child in busy {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
}
|
||||
let beside_children = beside_children.expect("sleeping beside busy children gave no figure");
|
||||
eprintln!(
|
||||
"process sleep vs compute: compute 2 ms = {compute:?}; sleep 2 ms = {sleep:?}; sleep 2 ms beside 4 busy \
|
||||
children = {beside_children:?}"
|
||||
);
|
||||
assert!(compute.uj > 0, "computing metered as zero: {compute:?}");
|
||||
assert!(sleep.uj * 10 < compute.uj, "sleeping metered {} uJ a call, computing {} uJ", sleep.uj, compute.uj);
|
||||
assert!(
|
||||
beside_children.uj * 10 < compute.uj,
|
||||
"sleeping beside busy child processes metered {} uJ a call, computing {} uJ: other processes were counted",
|
||||
beside_children.uj,
|
||||
compute.uj
|
||||
);
|
||||
} else {
|
||||
let o = oracle();
|
||||
assert!(!o.moved, "the oracle saw the process counter move ({o:?}), yet it was not selected");
|
||||
assert_ne!(measure_marginal(sleep_2ms).map(|m| m.source), Some(MeterSource::ProcessEnergy));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
//! The frozen path of the whole process-counter chain — probe, status, cache,
|
||||
//! selection, metering — on any host. The counter is live on the Macs that run
|
||||
//! these tests, so `simulate_frozen_process_energy()` makes this process's
|
||||
//! counter hold at its first value before the meter's first use, as a frozen
|
||||
//! counter would. This file holds this one test so that it runs in a process
|
||||
//! of its own and the hook cannot reach any other test.
|
||||
//!
|
||||
//! A selection that ignored the counter's liveness — choosing it because it
|
||||
//! reads, not because it keeps advancing — fails here.
|
||||
|
||||
use proof_cascade::meter::{
|
||||
ioreport_status, measure_marginal, meter_source, process_energy_status, real_process_probe_runs,
|
||||
simulate_frozen_process_energy, CounterStatus, IoReportStatus, MeterSource,
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Barrier};
|
||||
|
||||
/// How many times [`counted_work`] has run in this process.
|
||||
static WORK_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn counted_work() {
|
||||
WORK_CALLS.fetch_add(1, Ordering::SeqCst);
|
||||
std::hint::black_box((0..4000u64).fold(0u64, |a, b| a.wrapping_mul(31).wrapping_add(b)));
|
||||
}
|
||||
|
||||
/// With the counter holding, the status is `Frozen` for every thread that
|
||||
/// asks, the counter is never selected and never meters, and its probe ran
|
||||
/// exactly once. Whatever IOReport does is IOReport's own: selected only when
|
||||
/// its CPU rail is live, and then its figure says so; otherwise there is no
|
||||
/// figure and the work never runs.
|
||||
#[test]
|
||||
fn a_frozen_process_counter_is_never_selected_and_never_meters() {
|
||||
assert_eq!(real_process_probe_runs(), 0, "the meter was used in this process before this test");
|
||||
simulate_frozen_process_energy();
|
||||
const THREADS: usize = 4;
|
||||
let gate = Arc::new(Barrier::new(THREADS));
|
||||
let handles: Vec<_> = (0..THREADS)
|
||||
.map(|_| {
|
||||
let gate = gate.clone();
|
||||
std::thread::spawn(move || {
|
||||
gate.wait();
|
||||
let status = process_energy_status();
|
||||
let source = meter_source();
|
||||
(status, source)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let seen: Vec<_> = handles.into_iter().map(|h| h.join().expect("a thread panicked")).collect();
|
||||
let runs = real_process_probe_runs();
|
||||
let ioreport = ioreport_status();
|
||||
let figure = measure_marginal(counted_work);
|
||||
let work = WORK_CALLS.load(Ordering::SeqCst);
|
||||
eprintln!(
|
||||
"process frozen: {THREADS} threads saw {seen:?}; probe runs {runs}; ioreport={ioreport:?}; figure={figure:?}; \
|
||||
work calls {work}"
|
||||
);
|
||||
let want_source = (ioreport == IoReportStatus::Live).then_some(MeterSource::IoReport);
|
||||
for (i, (status, source)) in seen.iter().enumerate() {
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(*status, CounterStatus::Frozen, "thread {i}: a held counter was called {status:?}");
|
||||
assert_eq!(*source, want_source, "thread {i}: a held process counter was selected, or IOReport mis-ranked");
|
||||
} else {
|
||||
assert_eq!(*status, CounterStatus::Unavailable, "thread {i}: a process counter off macOS");
|
||||
assert_ne!(*source, Some(MeterSource::ProcessEnergy));
|
||||
}
|
||||
}
|
||||
assert_ne!(figure.map(|m| m.source), Some(MeterSource::ProcessEnergy), "a held process counter metered");
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(runs, 1, "the process-counter probe's runs in this process");
|
||||
match want_source {
|
||||
Some(src) => assert_eq!(figure.map(|m| m.source), Some(src), "IOReport was live, yet: {figure:?}"),
|
||||
None => {
|
||||
assert_eq!(figure, None, "no live source, yet a figure");
|
||||
assert_eq!(work, 0, "the work ran {work} times with no live source");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert_eq!(runs, 0, "a process-counter probe ran off macOS");
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@
|
|||
//! with the all-goals figure and its goal count beside it — the Amdahl
|
||||
//! breakdown by tier, the cache flywheel, and a sealed + verified JCR-1
|
||||
//! receipt. Only `--measure` attempts a hardware reading, and only through a
|
||||
//! counter the meter has seen to be live (on Apple Silicon IOReport's CPU
|
||||
//! energy rail, on Linux every RAPL package counter).
|
||||
//! counter the meter has seen to be live (on macOS this process's CPU energy
|
||||
//! counter, else IOReport's CPU energy rail; on Linux every RAPL package
|
||||
//! counter).
|
||||
|
||||
use ed25519_dalek::SigningKey;
|
||||
use proof_schema::{GoalKind, ProofGoal};
|
||||
|
|
@ -118,22 +119,29 @@ fn main() {
|
|||
summary.uj_by_tier[0], summary.uj_by_tier[1], summary.uj_by_tier[2], summary.uj_by_tier[3], t3,
|
||||
);
|
||||
|
||||
// --measure: try to put a hardware reading beside the wall-time model.
|
||||
// Bracket a real T0 proof against idle on the platform's energy interface
|
||||
// (IOReport on Apple Silicon, used only while its CPU energy rail is live;
|
||||
// RAPL on Linux, used only while every package counter is live) and report
|
||||
// the marginal joules. The figure is tagged `Energy::measured`, i.e.
|
||||
// HwShunt, on both platforms: a vendor energy counter's reading is a
|
||||
// hardware reading while the counter is live for the domain it claims
|
||||
// (SPEC §5), which the meter checks before it brackets anything. Where no
|
||||
// interface is usable, print why: the figures above then stay ModelBased.
|
||||
// --measure: try to put a hardware reading beside the wall-time model:
|
||||
// meter a real T0 proof with the first live source (`meter_source`) — on
|
||||
// macOS this process's CPU energy counter, else IOReport's Energy Model
|
||||
// (used only while its CPU energy rail is live); on Linux RAPL (used only
|
||||
// while every package counter is live) — and report the marginal joules.
|
||||
// The figure is graded by its source, HwShunt for each: a vendor or kernel
|
||||
// energy counter's reading is a hardware reading while the counter is live
|
||||
// for the domain it claims (SPEC §5), which the meter checks before it
|
||||
// meters anything; the figure's domain is printed beside it. Where no
|
||||
// source is live, print why: the figures above then stay ModelBased.
|
||||
if measure {
|
||||
use proof_cascade::meter::{hardware_meter_available, ioreport_status, measure_marginal_uj, rapl_status};
|
||||
let available = hardware_meter_available();
|
||||
use proof_cascade::meter::{
|
||||
ioreport_status, measure_marginal, meter_source, process_energy_status, rapl_status, MeterSource,
|
||||
};
|
||||
let source = meter_source();
|
||||
let available = source.is_some();
|
||||
// The decision THIS process made, printed so tests/measure.rs checks it
|
||||
// instead of re-probing in its own process, which could disagree.
|
||||
println!(
|
||||
"\n --measure: hardware meter available={available} · ioreport_status={:?} · rapl_status={:?}",
|
||||
"\n --measure: hardware meter available={available} · source={} · process_energy_status={:?} · \
|
||||
ioreport_status={:?} · rapl_status={:?}",
|
||||
source.map_or_else(|| "none".to_string(), |s| format!("{s:?}")),
|
||||
process_energy_status(),
|
||||
ioreport_status(),
|
||||
rapl_status()
|
||||
);
|
||||
|
|
@ -143,31 +151,60 @@ fn main() {
|
|||
no_hardware_reading_reason()
|
||||
);
|
||||
} else {
|
||||
// Why each source that is not used is not used: ranked below the
|
||||
// one used, or not live.
|
||||
if cfg!(target_os = "macos") && source == Some(MeterSource::ProcessEnergy) {
|
||||
let own = if ioreport_status() == proof_cascade::meter::IoReportStatus::Live {
|
||||
"its CPU energy rail is live too"
|
||||
} else {
|
||||
ioreport_reason()
|
||||
};
|
||||
println!(" --measure: IOReport not used: the process counter ranks above it. On its own: {own}.");
|
||||
} else if cfg!(target_os = "macos") {
|
||||
println!(" --measure: this process's CPU energy counter not used: {}.", process_reason());
|
||||
}
|
||||
// Meter a REAL, non-trivial T0 proof: validity of a 16-variable
|
||||
// propositional tautology, decided by the actual truth-table decider
|
||||
// (2^16 rows) — genuine, parallelizable compute, run on every core
|
||||
// and bracketed against idle. This is real proof work, not a spin.
|
||||
// (2^16 rows) — genuine, parallelizable compute, run on every core.
|
||||
// This is real proof work, not a spin.
|
||||
const HARD: &str =
|
||||
"(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p) | (~a&~b&~c&~d&~e&~f&~g&~h&~i&~j&~k&~l&~m&~n&~o&~p)";
|
||||
println!(" --measure: metering a real 16-variable T0 proof against idle on the energy interface…");
|
||||
println!(
|
||||
" --measure: metering a real 16-variable T0 proof, on every spare core, {}…",
|
||||
match source {
|
||||
Some(MeterSource::ProcessEnergy) => {
|
||||
"on this process's CPU energy counter (no idle baseline: other processes are not in it)"
|
||||
}
|
||||
_ => "against idle on the energy interface",
|
||||
}
|
||||
);
|
||||
let parsed = proof_solvers::prop::parse(HARD).expect("tautology parses");
|
||||
assert_eq!(proof_solvers::prop::is_valid(&parsed), Some(true), "the probe goal must be valid");
|
||||
let measured = measure_marginal_uj(|| {
|
||||
let measured = measure_marginal(|| {
|
||||
if let Some(e) = proof_solvers::prop::parse(HARD) {
|
||||
std::hint::black_box(proof_solvers::prop::is_valid(&e));
|
||||
}
|
||||
});
|
||||
match measured {
|
||||
Some(uj) if uj > 0 => {
|
||||
let e = proof_schema::Energy::measured(uj);
|
||||
Some(m) if m.uj > 0 => {
|
||||
let e = m.energy();
|
||||
let _ = e.check_provenance();
|
||||
println!(
|
||||
" measured: {uj} uj / proof · provenance {} (measured_uj present, the schema's strongest tier)",
|
||||
" measured: {} uj / proof · provenance {} (measured_uj present, the schema's strongest tier) · source {:?}",
|
||||
m.uj,
|
||||
e.provenance.jcr1_label(),
|
||||
m.source,
|
||||
);
|
||||
println!(" domain: {}", m.source.domain());
|
||||
println!(
|
||||
" this proof's joules are a hardware reading; the per-goal figures above stay ModelBased, and the receipt seals them."
|
||||
);
|
||||
println!(" the standard's headline (joules per verified proof) is now a real hardware reading, not a model.");
|
||||
}
|
||||
// The interface answered, and busy did not clear idle.
|
||||
// The process counter advanced by under half a microjoule a call.
|
||||
Some(m) if m.source == MeterSource::ProcessEnergy => println!(
|
||||
" measured: under 0.5 uj per proof, below this meter's 1 uj resolution — not a zero-energy proof."
|
||||
),
|
||||
// A bracket answered, and busy did not clear idle.
|
||||
Some(_) => println!(
|
||||
" measured: signal stayed inside the idle noise (a loaded/throttled box) — run on a quiet machine."
|
||||
),
|
||||
|
|
@ -215,36 +252,62 @@ fn main() {
|
|||
}
|
||||
|
||||
/// Why `--measure` has no hardware reading on this host. It comes from the
|
||||
/// meter's cached status — IOReport's on Apple Silicon, RAPL's on Linux — so a
|
||||
/// counter that is not live is named as such instead of being misread as a
|
||||
/// noisy machine.
|
||||
fn no_hardware_reading_reason() -> &'static str {
|
||||
use proof_cascade::meter::{ioreport_status, rapl_status, CounterStatus, IoReportStatus};
|
||||
/// meter's cached statuses — on Apple Silicon the process counter's and
|
||||
/// IOReport's, on Linux RAPL's — so a counter that is not live is named as
|
||||
/// such instead of being misread as a noisy machine.
|
||||
fn no_hardware_reading_reason() -> String {
|
||||
if cfg!(target_os = "macos") {
|
||||
match ioreport_status() {
|
||||
IoReportStatus::CpuRailFrozen => {
|
||||
"IOReport opens, but its CPU energy rail is frozen (it did not keep advancing while \
|
||||
this process burned CPU), so it cannot meter CPU work"
|
||||
}
|
||||
IoReportStatus::CpuRailAbsent => "IOReport's Energy Model group opens, but has no CPU energy rail",
|
||||
IoReportStatus::Unavailable => {
|
||||
"IOReport's Energy Model group could not be opened, or opened but could not be sampled \
|
||||
(opening it needs Apple Silicon, not Rosetta)"
|
||||
}
|
||||
IoReportStatus::Live => "IOReport's CPU energy rail is live, but the bracket returned no reading",
|
||||
}
|
||||
format!("{}; and {}", process_reason(), ioreport_reason())
|
||||
} else if cfg!(target_os = "linux") {
|
||||
match rapl_status() {
|
||||
CounterStatus::Unavailable => "no readable RAPL counter at /sys/class/powercap/intel-rapl:*/energy_uj",
|
||||
CounterStatus::Absent => "RAPL zones are readable, but none of them is a package zone",
|
||||
CounterStatus::Frozen => {
|
||||
"a RAPL package counter is readable but did not keep advancing while this process burned CPU, \
|
||||
so it cannot meter CPU work"
|
||||
}
|
||||
CounterStatus::Live => "RAPL's package counters are live, but the bracket returned no reading",
|
||||
}
|
||||
rapl_reason().to_string()
|
||||
} else {
|
||||
"no hardware energy interface is supported on this OS"
|
||||
"no hardware energy interface is supported on this OS".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Why this process's CPU energy counter is or is not used, from its status.
|
||||
fn process_reason() -> &'static str {
|
||||
use proof_cascade::meter::{process_energy_status, CounterStatus};
|
||||
match process_energy_status() {
|
||||
CounterStatus::Unavailable => {
|
||||
"this process's CPU energy counter (proc_pid_rusage, ri_energy_nj) could not be read"
|
||||
}
|
||||
CounterStatus::Absent => "this process's CPU energy counter is absent",
|
||||
CounterStatus::Frozen => {
|
||||
"this process's CPU energy counter did not keep advancing while this process burned CPU"
|
||||
}
|
||||
CounterStatus::Live => "this process's CPU energy counter is live, but the measurement returned no reading",
|
||||
}
|
||||
}
|
||||
|
||||
/// Why IOReport is or is not used, from its status.
|
||||
fn ioreport_reason() -> &'static str {
|
||||
use proof_cascade::meter::{ioreport_status, IoReportStatus};
|
||||
match ioreport_status() {
|
||||
IoReportStatus::CpuRailFrozen => {
|
||||
"IOReport opens, but its CPU energy rail is frozen (it did not keep advancing while \
|
||||
this process burned CPU), so it cannot meter CPU work"
|
||||
}
|
||||
IoReportStatus::CpuRailAbsent => "IOReport's Energy Model group opens, but has no CPU energy rail",
|
||||
IoReportStatus::Unavailable => {
|
||||
"IOReport's Energy Model group could not be opened, or opened but could not be sampled \
|
||||
(opening it needs Apple Silicon, not Rosetta)"
|
||||
}
|
||||
IoReportStatus::Live => "IOReport's CPU energy rail is live, but the bracket returned no reading",
|
||||
}
|
||||
}
|
||||
|
||||
/// Why RAPL is or is not used, from its status.
|
||||
fn rapl_reason() -> &'static str {
|
||||
use proof_cascade::meter::{rapl_status, CounterStatus};
|
||||
match rapl_status() {
|
||||
CounterStatus::Unavailable => "no readable RAPL counter at /sys/class/powercap/intel-rapl:*/energy_uj",
|
||||
CounterStatus::Absent => "RAPL zones are readable, but none of them is a package zone",
|
||||
CounterStatus::Frozen => {
|
||||
"a RAPL package counter is readable but did not keep advancing while this process burned CPU, \
|
||||
so it cannot meter CPU work"
|
||||
}
|
||||
CounterStatus::Live => "RAPL's package counters are live, but the bracket returned no reading",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
//! `proof --measure`, end to end: when the hardware meter declines, the output
|
||||
//! names the reason, nothing in it claims a hardware reading, and the cascade's
|
||||
//! own ModelBased figures stand; when it meters, the host really has a usable
|
||||
//! interface. 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 rail could disagree.
|
||||
//! `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 status is itself checked against what the process did with it.
|
||||
//! The CLI's reason for declining comes from the status it acted on, so on
|
||||
//! macOS every IOReport reason in the output must be the one for the printed
|
||||
//! status, and a declined meter must print that reason: a CLI that prints one
|
||||
//! status and acts on another fails here, whichever status either one is.
|
||||
//! 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;
|
||||
|
||||
|
|
@ -20,8 +27,8 @@ fn field<'a>(stdout: &'a str, key: &str) -> Option<&'a str> {
|
|||
}
|
||||
|
||||
/// Every `IoReportStatus` the CLI can print, each with the reason it gives
|
||||
/// when that status is why there is no hardware reading (`Live`'s is for a
|
||||
/// bracket that returned nothing). Each reason names exactly one status.
|
||||
/// 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"),
|
||||
|
|
@ -29,6 +36,15 @@ const IOREPORT_REASONS: [(&str, &str); 4] = [
|
|||
("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] = [
|
||||
|
|
@ -38,8 +54,63 @@ const RAPL_REASONS: [(&str, &str); 4] = [
|
|||
("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_why_it_declined_and_claims_no_hardware_reading() {
|
||||
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()
|
||||
|
|
@ -56,24 +127,34 @@ fn measure_names_why_it_declined_and_claims_no_hardware_reading() {
|
|||
Some("false") => false,
|
||||
other => panic!("proof --measure did not print its meter decision ({other:?}):\n{stdout}"),
|
||||
};
|
||||
let status = field(&stdout, "ioreport_status=")
|
||||
.unwrap_or_else(|| panic!("proof --measure did not print its IOReport status:\n{stdout}"));
|
||||
assert!(
|
||||
IOREPORT_REASONS.iter().any(|&(s, _)| s == status),
|
||||
"proof --measure printed an unknown IOReport status {status:?}:\n{stdout}"
|
||||
);
|
||||
let rapl = field(&stdout, "rapl_status=")
|
||||
.unwrap_or_else(|| panic!("proof --measure did not print its RAPL status:\n{stdout}"));
|
||||
assert!(
|
||||
RAPL_REASONS.iter().any(|&(s, _)| s == rapl),
|
||||
"proof --measure printed an unknown RAPL status {rapl:?}:\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} ioreport_status={status} rapl_status={rapl}");
|
||||
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: ModelBased and non-zero, never the --measure figure.
|
||||
// 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"))
|
||||
|
|
@ -85,25 +166,21 @@ fn measure_names_why_it_declined_and_claims_no_hardware_reading() {
|
|||
.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!(available, rapl == "Live", "on Linux the meter is available exactly when RAPL is Live:\n{stdout}");
|
||||
for (reason_status, reason) in RAPL_REASONS {
|
||||
if stdout.contains(reason) {
|
||||
assert_eq!(
|
||||
reason_status, rapl,
|
||||
"the CLI printed rapl_status={rapl} but gave the reason for {reason_status}:\n{stdout}"
|
||||
);
|
||||
}
|
||||
}
|
||||
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 {
|
||||
let (_, reason) = RAPL_REASONS.iter().find(|&&(s, _)| s == rapl).expect("a known status");
|
||||
assert!(stdout.contains(reason), "declined with rapl_status={rapl}, but not for its reason:\n{stdout}");
|
||||
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}");
|
||||
|
|
@ -112,52 +189,56 @@ fn measure_names_why_it_declined_and_claims_no_hardware_reading() {
|
|||
}
|
||||
}
|
||||
if cfg!(target_os = "macos") {
|
||||
assert_eq!(
|
||||
available,
|
||||
status == "Live",
|
||||
"on macOS the meter is available exactly when IOReport is Live:\n{stdout}"
|
||||
);
|
||||
// The reason is derived from the status the process acted on: it must
|
||||
// be the printed status's own, and no other status's.
|
||||
for (reason_status, reason) in IOREPORT_REASONS {
|
||||
if stdout.contains(reason) {
|
||||
// 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!(
|
||||
reason_status, status,
|
||||
"the CLI printed ioreport_status={status} but gave the reason for {reason_status}:\n{stdout}"
|
||||
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}");
|
||||
}
|
||||
}
|
||||
if declined {
|
||||
let (_, reason) = IOREPORT_REASONS.iter().find(|&&(s, _)| s == status).expect("a known status");
|
||||
assert!(
|
||||
stdout.contains(reason),
|
||||
"the meter declined with ioreport_status={status}, but not for that status's reason ({reason:?}):\n{stdout}"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Off macOS there is no IOReport: the status is always Unavailable,
|
||||
// and no IOReport reason is ever given.
|
||||
// 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}");
|
||||
for (reason_status, reason) in IOREPORT_REASONS {
|
||||
assert!(
|
||||
!stdout.contains(reason),
|
||||
"an IOReport reason ({reason_status}) 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("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}"
|
||||
|
|
@ -168,4 +249,27 @@ fn measure_names_why_it_declined_and_claims_no_hardware_reading() {
|
|||
"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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue