0d78becb's rules were tested on scripted counters. These two hold them against this process's own CPU energy counter, each in a test binary of its own with one test, as eoc-meter's are: the counter's domain is every thread of the process, and the oracle's check is the process's context-switch count. Gated the same way: macOS only, and the counter must be live. The oracle (`tests/oracle`) shares no call and no number with the counter: `proc_pid_rusage` declared there and read at the SDK's offset, taken once the test's thread has left its core by the process's context-switch count rising across a sleep. A `Watched` counter puts that read on either side of every read the cascade takes, so a span lies between two windows of the oracle's: the one inside it and the one around it and both reads. That a reading lies between them holds for any count that only grows, so it is asserted every time; what is allowed to miss a tenth of the time is how far apart the two windows are, which is the two readers' own reads and wake-ups. `hit_calibration_process_energy`: - each tier calibrated 20 times through the watched counter: the span the calibration kept is its last two reads, it lies between the oracle's two windows around the same batch, the per-hit figure times the batch is that span, and the empty span it reports is the counter's first advance over no work. The inner window was 952/1000 of the outer at least, quiet and under 40 busy loops, so the figure is pinned within a twentieth; a fifth is asked. - on the bare counter, after a tier's first hit, 10,000 hits take no read of the meter (counted), every one is an estimate, and the microjoules charged are the calibrated nanojoules to the one microjoule still carried. - what the change removes: a span around one hit, two reads about one lookup, reads more than the hit is charged, every time; in an optimized build 5.8 to 17 times as much at the median, and three times is asked. In an unoptimized build the lookup itself costs about what the meter does (22 µJ a cache hit against 1.4 optimized), so no multiple is asked there. `neural_span_process_energy`, with a KV stage that burns 8 ms of the thread's CPU on its way to missing: - a neural stage that burns 2 ms: exactly two reads, the first after the KV lookup, and the measured figure between the oracle's window inside the span and its window around it, which opens after the burn. The inner window was 785/1000 of the outer at least under load (992 quiet); two thirds is asked. - a neural stage that only sleeps 2 ms, on the bare counter, nothing taking the thread off its core between the burn and the cascade's first read: the figure is under a quarter of the oracle's window over the whole resolve (at most 32/1000 of it). A span opened before the walk reads the burn. No joule figure and no timing is asserted. A calibration that lands on an efficiency core reads a fifth of what one on a performance core does (253 nJ a cache hit against 1,400): the figure follows the core the batch ran on. Co-Authored-By: Claude Opus 5.5 <[email protected]>
213 lines
7.6 KiB
Rust
213 lines
7.6 KiB
Rust
//! The tests' own reading of this process's CPU energy, shared by the test
|
|
//! binaries of this directory that hold the cascade's figures against it.
|
|
//!
|
|
//! It shares no call and no number with the counter under test:
|
|
//! `proc_pid_rusage` is declared here and read into raw words at the SDK's
|
|
//! offset, and a read is taken only once this thread has left its core by a
|
|
//! rule of the oracle's own — the process's context-switch count rose across
|
|
//! a sleep — where the counter watches the reading thread's own energy count.
|
|
//! That rule holds only while one thread of the process runs, so each binary
|
|
//! here has one test.
|
|
|
|
#![allow(dead_code)]
|
|
|
|
use std::os::raw::{c_int, c_void};
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use eoc_meter::JouleCounter;
|
|
|
|
unsafe extern "C" {
|
|
fn proc_pid_rusage(pid: c_int, flavor: c_int, buffer: *mut c_void) -> c_int;
|
|
fn proc_pidinfo(pid: c_int, flavor: c_int, arg: u64, buffer: *mut c_void, buffersize: c_int) -> c_int;
|
|
fn clock_gettime_nsec_np(clock_id: u32) -> u64;
|
|
}
|
|
|
|
/// `CLOCK_THREAD_CPUTIME_ID` in macOS `<time.h>`.
|
|
const CLOCK_THREAD_CPUTIME_ID: u32 = 16;
|
|
|
|
/// This thread's CPU time.
|
|
fn thread_cpu() -> Duration {
|
|
Duration::from_nanos(unsafe { clock_gettime_nsec_np(CLOCK_THREAD_CPUTIME_ID) })
|
|
}
|
|
|
|
/// Spin this thread until it has used `d` of its own CPU time (or 20 s of
|
|
/// wall time have passed), however loaded the host is.
|
|
pub fn burn(d: Duration) {
|
|
let (started, wall) = (thread_cpu(), Instant::now());
|
|
let mut acc: u64 = 0;
|
|
while thread_cpu() - started < d && wall.elapsed() < Duration::from_secs(20) {
|
|
for _ in 0..1_000 {
|
|
acc = std::hint::black_box(acc.wrapping_add(1));
|
|
}
|
|
}
|
|
}
|
|
|
|
fn me() -> c_int {
|
|
std::process::id() as c_int
|
|
}
|
|
|
|
/// This process's `ri_energy_nj` as it stands: `proc_pid_rusage(pid,
|
|
/// RUSAGE_INFO_V6)` into 464 raw bytes, the count at byte 336.
|
|
fn energy_nj() -> u64 {
|
|
let mut words = [0u64; 464 / 8];
|
|
let rc = unsafe { proc_pid_rusage(me(), 6, words.as_mut_ptr() as *mut c_void) };
|
|
assert_eq!(rc, 0, "the oracle's proc_pid_rusage");
|
|
words[336 / 8]
|
|
}
|
|
|
|
/// How many times this process's threads have been switched off a core:
|
|
/// `pti_csw`, the `int32_t` at byte 80 of the 96-byte `proc_taskinfo` that
|
|
/// `proc_pidinfo(pid, PROC_PIDTASKINFO = 4)` fills.
|
|
fn context_switches() -> i32 {
|
|
let mut info = [0i32; 96 / 4];
|
|
let wrote = unsafe { proc_pidinfo(me(), 4, 0, info.as_mut_ptr() as *mut c_void, 96) };
|
|
assert_eq!(wrote, 96, "the oracle's proc_pidinfo(PROC_PIDTASKINFO)");
|
|
info[80 / 4]
|
|
}
|
|
|
|
/// The oracle's read of this process's energy, in nanojoules, taken once
|
|
/// this thread has left its core: a sleep of 200 µs, then 1, 2, 4 and 8 ms,
|
|
/// until the process's context-switch count rose across one. Only the test's
|
|
/// thread runs in the process, so a rise is this thread's switch, and the
|
|
/// read after it holds all the thread spent before it.
|
|
pub fn energy_nj_off_core() -> u64 {
|
|
let left = [200, 1_000, 2_000, 4_000, 8_000].into_iter().any(|micros| {
|
|
let before = context_switches();
|
|
std::thread::sleep(Duration::from_micros(micros));
|
|
context_switches() > before
|
|
});
|
|
assert!(left, "the oracle never saw this thread leave its core across sleeps of 0.2, 1, 2, 4 and 8 ms");
|
|
energy_nj()
|
|
}
|
|
|
|
/// One read of a [`Watched`] counter: the oracle's reading before it, what
|
|
/// the counter read (`None` for a failed read), the oracle's after it, and
|
|
/// where the test's own count stood when the read was asked for.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct Read {
|
|
pub before_nanojoules: u64,
|
|
pub microjoules: Option<u64>,
|
|
pub after_nanojoules: u64,
|
|
pub count: u64,
|
|
}
|
|
|
|
/// A counter with the oracle's own read on either side of every read of it,
|
|
/// kept in order. A span between two of its reads therefore lies between two
|
|
/// windows of the oracle's: the one inside the span, from the oracle's read
|
|
/// after the opening read to its read before the closing one, and the one
|
|
/// around the span and both reads.
|
|
///
|
|
/// The oracle's reads between the two readings of a span are the span's too:
|
|
/// its sleep after the opening read and before the closing one. So a span
|
|
/// through this counter reads a little more than one through the bare
|
|
/// counter would, and both windows hold it.
|
|
pub struct Watched<C> {
|
|
counter: C,
|
|
reads: Mutex<Vec<Read>>,
|
|
count: Arc<AtomicU64>,
|
|
}
|
|
|
|
impl<C: JouleCounter> Watched<C> {
|
|
pub fn new(counter: C) -> Self {
|
|
Self::beside(counter, Arc::new(AtomicU64::new(0)))
|
|
}
|
|
|
|
/// A watched counter that also notes, at each read, where `count`
|
|
/// stood: a count the test moves, to place a read among its own steps.
|
|
pub fn beside(counter: C, count: Arc<AtomicU64>) -> Self {
|
|
Self { counter, reads: Mutex::new(Vec::new()), count }
|
|
}
|
|
|
|
/// The reads taken since the last call, in order.
|
|
pub fn take(&self) -> Vec<Read> {
|
|
std::mem::take(&mut *self.reads.lock().unwrap())
|
|
}
|
|
}
|
|
|
|
impl<C: JouleCounter> JouleCounter for Watched<C> {
|
|
fn read_microjoules(&self) -> eoc_core::Result<u64> {
|
|
let count = self.count.load(Ordering::SeqCst);
|
|
let before_nanojoules = energy_nj_off_core();
|
|
let read = self.counter.read_microjoules();
|
|
let after_nanojoules = energy_nj_off_core();
|
|
self.reads.lock().unwrap().push(Read {
|
|
before_nanojoules,
|
|
microjoules: read.as_ref().ok().copied(),
|
|
after_nanojoules,
|
|
count,
|
|
});
|
|
read
|
|
}
|
|
fn name(&self) -> &'static str {
|
|
self.counter.name()
|
|
}
|
|
}
|
|
|
|
/// A span between two reads of a [`Watched`] counter, in nanojoules: what
|
|
/// the counter read, and the oracle's two windows.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct Span {
|
|
/// The counter's two reads' delta; `None` when either failed or the
|
|
/// counter did not advance.
|
|
pub read: Option<u64>,
|
|
/// The oracle's window inside the span.
|
|
pub inside: u64,
|
|
/// The oracle's window around the span and both of its reads.
|
|
pub around: u64,
|
|
}
|
|
|
|
impl Span {
|
|
pub fn between(open: &Read, close: &Read) -> Self {
|
|
let read = match (open.microjoules, close.microjoules) {
|
|
(Some(open), Some(close)) if close > open => Some((close - open) * 1_000),
|
|
_ => None,
|
|
};
|
|
Self {
|
|
read,
|
|
inside: close.before_nanojoules - open.after_nanojoules,
|
|
around: close.after_nanojoules - open.before_nanojoules,
|
|
}
|
|
}
|
|
|
|
/// Whether `nanojoules`, a figure for this span, lies between the
|
|
/// oracle's two windows, give or take the micro-joule the counter's two
|
|
/// reads round away. The count only grows, so a reading of the span that
|
|
/// does not is not this process's count.
|
|
pub fn holds(&self, nanojoules: u64) -> bool {
|
|
self.inside < nanojoules + 1_000 && nanojoules < self.around + 1_000
|
|
}
|
|
|
|
/// The window inside the span as thousandths of the one around it: how
|
|
/// closely the oracle pins the span.
|
|
pub fn tightness(&self) -> u64 {
|
|
self.inside * 1_000 / self.around.max(1)
|
|
}
|
|
}
|
|
|
|
/// A counter that counts its reads.
|
|
pub struct Counted<C> {
|
|
counter: C,
|
|
reads: Mutex<u64>,
|
|
}
|
|
|
|
impl<C: JouleCounter> Counted<C> {
|
|
pub fn new(counter: C) -> Self {
|
|
Self { counter, reads: Mutex::new(0) }
|
|
}
|
|
|
|
pub fn reads(&self) -> u64 {
|
|
*self.reads.lock().unwrap()
|
|
}
|
|
}
|
|
|
|
impl<C: JouleCounter> JouleCounter for Counted<C> {
|
|
fn read_microjoules(&self) -> eoc_core::Result<u64> {
|
|
*self.reads.lock().unwrap() += 1;
|
|
self.counter.read_microjoules()
|
|
}
|
|
fn name(&self) -> &'static str {
|
|
self.counter.name()
|
|
}
|
|
}
|