Every interceptor step was metered as a span of its own. Where the meter reads this process's energy count, each read first takes the reading thread off its core (#229): tens of microseconds and its own wake-up. An interceptor step is often a few microseconds, so a span of its own read as mostly the meter, and a chain of k interceptors paid 2k reads per seam. A meter that reads a counter now meters the whole seam pass as one span (two reads per seam), and the steps are sealed after it closes, each with its share of the reading by its own thread CPU time, which is exact and read at no cost. The shares are integer micro-joules that sum to the reading exactly (largest remainder); a share of a reading is ModelBased, a pass of one step keeps the reading's own grade, and coverage is the pass's. A meter that reads nothing (a model, a constant) still meters each step on its own, so the receipts sealed with it are byte-identical. jcp-energy exposes the thread CPU clock it already had (`thread_cpu_time`). On the M3 Ultra, three steps burning 2, 4 and 8 ms were sealed 10.8, 21.5 and 42.8 mJ, summing to the chain's figure and to 96-97% of an independent read around the pass, in 10 of 10 rounds. Co-Authored-By: Claude Opus 5.5 <[email protected]>
115 lines
5.1 KiB
Rust
115 lines
5.1 KiB
Rust
//! A seam metered on this process's own energy count, as one span around the
|
|
//! whole pass, against an oracle that shares no code with the meter.
|
|
//!
|
|
//! Three interceptors each burn a different amount of their thread's CPU time
|
|
//! in one `before_model` pass. Each step is sealed with its share of the
|
|
//! pass's reading by thread CPU time (`ModelBased`), the shares sum to the
|
|
//! chain's figure, and that figure is the process's own energy over the pass:
|
|
//! no more than an oracle's window around it, and most of it. The oracle takes
|
|
//! its thread off its core its own way before each read (sleeps until the
|
|
//! process's context-switch count moves), then reads `ri_energy_nj` into a raw
|
|
//! buffer at the SDK's offset.
|
|
|
|
#![cfg(target_os = "macos")]
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_energy::Meter;
|
|
use jcp_intercept::{verify, Chain, Interceptor, ModelRequest, Step};
|
|
use jcp_schema::Provenance;
|
|
use std::time::Duration;
|
|
|
|
/// Burn `cpu` of this thread's own CPU time.
|
|
fn burn(cpu: Duration) {
|
|
let start = jcp_energy::thread_cpu_time().expect("the thread clock");
|
|
let mut x = 1u64;
|
|
while jcp_energy::thread_cpu_time().expect("the thread clock") - start < cpu {
|
|
for _ in 0..10_000 {
|
|
x = std::hint::black_box(x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Burner {
|
|
name: &'static str,
|
|
cpu: Duration,
|
|
}
|
|
|
|
impl Interceptor for Burner {
|
|
fn name(&self) -> &str {
|
|
self.name
|
|
}
|
|
fn before_model(&mut self, _req: &mut ModelRequest) -> Step {
|
|
burn(self.cpu);
|
|
Step::Continue
|
|
}
|
|
}
|
|
|
|
/// This process's `ri_energy_nj` (`proc_pid_rusage`, `RUSAGE_INFO_V6`, the
|
|
/// u64 at byte 336), after this thread has left its core: sleep 1, 2, 4, 8 ms
|
|
/// until the process's context-switch count (`pti_csw` would do; the rusage
|
|
/// `ri_user_time` is not enough) has moved. `ru_nvcsw + ru_nivcsw` of this
|
|
/// process is read with `getrusage`.
|
|
fn oracle_nj() -> u64 {
|
|
fn switches() -> i64 {
|
|
// SAFETY: `ru` is a valid, writable `rusage`.
|
|
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
|
|
assert_eq!(unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut ru) }, 0);
|
|
ru.ru_nvcsw + ru.ru_nivcsw
|
|
}
|
|
let before = switches();
|
|
for ms in [1, 2, 4, 8] {
|
|
std::thread::sleep(Duration::from_millis(ms));
|
|
if switches() > before {
|
|
break;
|
|
}
|
|
}
|
|
let mut buf = [0u64; 58];
|
|
// SAFETY: `buf` is 464 writable bytes, the size of `rusage_info_v6`.
|
|
let rc = unsafe { libc::proc_pid_rusage(libc::getpid(), 6, buf.as_mut_ptr() as *mut libc::rusage_info_t) };
|
|
assert_eq!(rc, 0, "proc_pid_rusage");
|
|
buf[336 / 8]
|
|
}
|
|
|
|
#[test]
|
|
fn a_pass_is_one_reading_divided_among_its_steps_by_cpu_time() {
|
|
let meter = Meter::detect();
|
|
assert_eq!(meter.provenance(), Provenance::HwShunt, "this host's meter does not read a counter: {:?}", meter.rung());
|
|
let steward = SigningKey::from_bytes(&[7u8; 32]);
|
|
let mut misses = 0;
|
|
for round in 0..10 {
|
|
let mut c = Chain::new(steward.clone(), "did:jcp:test-intercept-pass")
|
|
.with_meter(Meter::detect())
|
|
.with(Box::new(Burner { name: "one", cpu: Duration::from_millis(2) }))
|
|
.with(Box::new(Burner { name: "two", cpu: Duration::from_millis(4) }))
|
|
.with(Box::new(Burner { name: "four", cpu: Duration::from_millis(8) }));
|
|
let before = oracle_nj();
|
|
assert!(c.before_model(&mut ModelRequest::default()).proceeds());
|
|
let oracle = oracle_nj() - before;
|
|
|
|
let recs: Vec<_> =
|
|
c.receipts().iter().map(|e| verify(e, c.steward(), &c.steward_pubkey()).expect("a sealed step verifies")).collect();
|
|
assert_eq!(recs.iter().map(|r| r.interceptor.as_str()).collect::<Vec<_>>(), ["one", "two", "four"]);
|
|
let figures: Vec<u64> = recs.iter().map(|r| r.energy_uj).collect();
|
|
let sum: u64 = figures.iter().sum();
|
|
eprintln!(
|
|
"intercept pass: round {round}: steps {figures:?} µJ ({:?}), sum {sum} µJ, chain {} µJ, oracle window {:.0} µJ",
|
|
recs.iter().map(|r| r.provenance.as_str()).collect::<Vec<_>>(),
|
|
c.energy_uj(),
|
|
oracle as f64 / 1e3
|
|
);
|
|
assert_eq!(sum, c.energy_uj(), "the steps' shares are not the chain's figure");
|
|
if recs.iter().any(|r| r.provenance != "ModelBased") {
|
|
// A pass whose ends could not be read is the estimate, divided as one.
|
|
assert!(recs.iter().all(|r| r.provenance == "Estimator"), "{recs:?}");
|
|
misses += 1;
|
|
continue;
|
|
}
|
|
// The shares follow the steps' own CPU time: 2 : 4 : 8 ms.
|
|
assert!(figures[0] < figures[1] && figures[1] < figures[2], "{figures:?}");
|
|
// The pass is the process's own energy over it: within the oracle's
|
|
// window, and most of it.
|
|
assert!(sum * 1_000 <= oracle + 1_000, "the pass ({sum} µJ) exceeds the oracle's window ({oracle} nJ)");
|
|
assert!(sum * 1_000 * 3 >= oracle * 2, "the pass ({sum} µJ) is under two thirds of the oracle's window ({oracle} nJ)");
|
|
}
|
|
assert!(misses <= 1, "{misses} of 10 passes could not be read");
|
|
}
|