//! 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::>(), ["one", "two", "four"]); let figures: Vec = 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::>(), 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"); }