jcp-energy: compare two threads' energy within one span, so host load no longer decides the test #179
1 changed files with 94 additions and 14 deletions
|
|
@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue