diff --git a/jcp/jcp-rs/crates/jcp-energy/src/lib.rs b/jcp/jcp-rs/crates/jcp-energy/src/lib.rs index 930183d30..167903ada 100644 --- a/jcp/jcp-rs/crates/jcp-energy/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-energy/src/lib.rs @@ -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 + /// `` (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()); }); }