jcp-energy: compare two threads' energy within one span, so host load no longer decides the test #179

Merged
dcharlot merged 12 commits from ci/energy-tests-load-independent into main 2026-10-07 21:21:28 -04:00

View file

@ -7250,6 +7250,14 @@ mod tests {
buffer: *mut std::ffi::c_void,
buffersize: std::os::raw::c_int,
) -> std::os::raw::c_int;
fn proc_pidinfo(
pid: std::os::raw::c_int,
flavor: std::os::raw::c_int,
arg: u64,
buffer: *mut std::ffi::c_void,
buffersize: std::os::raw::c_int,
) -> std::os::raw::c_int;
fn pthread_threadid_np(thread: *mut std::ffi::c_void, thread_id: *mut u64) -> std::os::raw::c_int;
}
/// This thread's CPU time, by the tests' own clock.
@ -7309,6 +7317,56 @@ mod tests {
})
}
/// `proc_pidinfo` flavor 34, `PROC_PIDTHREADCOUNTS` in the kernel's
/// `<sys/proc_info.h>` (the SDK's copy of that header leaves it out): one
/// thread's counts, asked by its 64-bit thread id. The kernel writes a
/// `uint16_t` count of CPU performance levels and six bytes of padding,
/// then for each level five `uint64_t`: instructions, cycles, user time,
/// system time, and the thread's CPU energy in nJ. On the dev host: two
/// levels, 88 bytes.
#[cfg(target_os = "macos")]
const TESTS_PIDTHREADCOUNTS: std::os::raw::c_int = 34;
/// The tests' own read of the calling thread's own CPU energy count, in
/// nJ, summed over the performance levels it ran on: the per-thread
/// oracle, through their own FFI declarations. The meter reads the whole
/// process's count by another call, and the kernel keeps that count as
/// its threads' counts added up, an exited thread's included. On the dev
/// host, under a load average near 100, a span in which one thread burned
/// rose by exactly that thread's own rise, and one in which two did, by
/// their sum and a little more (spawning and joining the second): never
/// less, in every span measured.
#[cfg(target_os = "macos")]
#[allow(unsafe_code)]
fn oracle_this_thread_energy_nj() -> u64 {
const LEVELS: usize = 8;
let mut tid = 0u64;
let rc = unsafe { pthread_threadid_np(std::ptr::null_mut(), &mut tid) };
assert_eq!(rc, 0, "the tests' pthread_threadid_np failed");
let mut counts = [0u64; 1 + 5 * LEVELS];
let bytes = std::mem::size_of_val(&counts) as std::os::raw::c_int;
let me = std::process::id() as std::os::raw::c_int;
let wrote = unsafe { proc_pidinfo(me, TESTS_PIDTHREADCOUNTS, tid, counts.as_mut_ptr().cast(), bytes) };
let head = counts[0].to_ne_bytes();
let levels = usize::from(u16::from_ne_bytes([head[0], head[1]]));
assert!(
(1..=LEVELS).contains(&levels) && usize::try_from(wrote) == Ok(8 + 40 * levels),
"the tests' per-thread counts: {wrote} bytes written for {levels} performance level(s)"
);
(0..levels).map(|level| counts[1 + 5 * level + 4]).sum()
}
/// [`burn_thread_cpu`], with the rise of this thread's own energy count
/// across the burn by the per-thread oracle, in nJ: the wall time the burn
/// took, and that rise.
#[cfg(target_os = "macos")]
fn burn_thread_cpu_counted(cpu: Duration) -> (Duration, u64) {
let before = oracle_this_thread_energy_nj();
let wall = burn_thread_cpu(cpu);
let after = oracle_this_thread_energy_nj();
(wall, after.checked_sub(before).expect("this thread's own energy count fell"))
}
/// The tests' own read of a process's reaped children's CPU time
/// (`ri_child_user_time` + `ri_child_system_time`), through their own FFI
/// declaration and the transcribed struct.
@ -7497,33 +7555,55 @@ mod tests {
/// Two threads burning 200 ms of CPU each meter more than one: every
/// thread of the process is in the count. Each thread's 200 ms are of its
/// own CPU clock (see [`burn_thread_cpu`]), so neither span's work depends
/// on how loaded the host is.
/// own CPU clock (see [`burn_thread_cpu`]). Both burn in one span, and the
/// tests' per-thread oracle reads each one's own count across its burn.
/// The span's figure must be at least the two counts added up, and so
/// more than either one's: what one thread burning that CPU would meter.
///
/// The figures compared come from the same span, so whichever cores and
/// clocks a loaded host gives the threads, they count the same work. Two
/// spans one after the other do not: the CPU is the same in both, but its
/// energy follows the core type and clock each span ran on, and on a
/// loaded host one thread's 200 ms read 0.92 J and two threads' 0.59 J.
/// A meter that counts only the thread that calls it reads one thread
/// here, and fails.
#[cfg(target_os = "macos")]
#[test]
fn two_threads_burning_meter_more_than_one() {
in_a_fresh_process("tests::two_threads_burning_meter_more_than_one", || {
let meter = process_meter();
let cpu = Duration::from_millis(200);
let (wall_one, one) = meter.measure(|| burn_thread_cpu(cpu));
let (wall_two, two) = meter.measure(|| {
let (((wall_mine, mine_nj), (wall_other, other_nj)), two) = meter.measure(|| {
std::thread::scope(|s| {
let other = s.spawn(|| burn_thread_cpu(cpu));
let mine = burn_thread_cpu(cpu);
mine.max(other.join().expect("the second burning thread panicked"))
let other = s.spawn(|| burn_thread_cpu_counted(cpu));
let mine = burn_thread_cpu_counted(cpu);
(mine, other.join().expect("the second burning thread panicked"))
})
});
let threads_nj = mine_nj + other_nj;
eprintln!(
"process energy: 200 ms of CPU on 1 thread, in {wall_one:?} of wall time -> {} µJ ({:.3} J), on each \
of 2 threads, in {wall_two:?} -> {} µJ ({:.3} J), ratio {:.2}",
one.uj(),
joules(one.uj()),
"process energy: 200 ms of CPU on each of 2 threads, in {wall_mine:?} and {wall_other:?} of wall \
time -> {} µJ ({:.3} J); the threads' own counts rose {mine_nj} + {other_nj} nJ ({:.3} J), ratio \
{:.3}",
two.uj(),
joules(two.uj()),
two.uj() as f64 / one.uj().max(1) as f64
threads_nj as f64 / 1e9,
two.uj() as f64 * 1e3 / threads_nj.max(1) as f64
);
assert_eq!(two.provenance, Provenance::HwShunt, "{two:?}");
assert!(mine_nj > 0 && other_nj > 0, "a burning thread's own count did not rise: {mine_nj} and {other_nj} nJ");
let metered_nj = two.uj().saturating_mul(1_000);
assert!(
metered_nj > mine_nj.max(other_nj),
"two threads metered {} µJ, no more than one of them alone ({mine_nj} and {other_nj} nJ)",
two.uj()
);
assert!(
metered_nj >= threads_nj,
"two threads metered {} µJ, under their own counts added up, {mine_nj} + {other_nj} = {threads_nj} nJ: \
a thread of the process is not in the count",
two.uj()
);
assert_eq!((one.provenance, two.provenance), (Provenance::HwShunt, Provenance::HwShunt));
assert!(two.uj() > one.uj(), "two threads metered {} µJ, one thread {} µJ", two.uj(), one.uj());
});
}