open-standards/eoc/eoc-rs/crates/eoc-meter/tests/rapl_hardware.rs
Transaction Science 335f7f1fa7
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m35s
energy meters on real hardware / sink decode table · per-task energy and the GPU rail on macOS (pull_request) Successful in 2m46s
standards workspaces build + test / eoc-rs (eoc/eoc-rs, -p eoc-meter --features cuda, eoc-rs, linux) (pull_request) Successful in 2m43s
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 36s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m57s
energy meters on real hardware / sink decode table · RAPL on jetson-hub (NVML surveyed) (pull_request) Successful in 2m22s
eoc-meter: RAPL MSR domains are per die on Intel, per package on AMD; powercap die zones count
The kernel scopes Intel's RAPL energy counters per die. Its perf PMU treats
the package counter as package-scope only on AMD and Hygon ("AMD/HYGON
platforms have a per-PKG package energy counter", rapl_pkg_pmu_is_pkg_scope()
in arch/x86/events/rapl.c), and on a multi-die Intel package (Cascade Lake-AP)
"its RAPL MSRs are die-scope"; its powercap driver then lists one zone per die,
named package-<p>-die-<d> (intel_rapl_common.c). Both paths here under-counted
such a package, the unsafe direction at a ceiling:

- The MSR fallback grouped CPUs by physical_package_id alone, so it read one
  die of a multi-die Intel package. On Intel a counter domain is now
  (physical_package_id, die_id), die_id 0 where the file is absent; on AMD
  and Hygon it stays the package. Each domain is read on its lowest-numbered
  CPU and every domain is summed, all or nothing as before (zero unit, wrap,
  powercap preferred, status text unchanged). A die of a multi-die package
  has its own rail name, package-<p>-die-<d> (MSR), in the liveness rails and
  in errors.
- The powercap name rule accepted only package-<n>, so such a host's die
  zones were no package at all. It now also accepts package-<p>-die-<d>, and
  nothing looser (not psys, dram, package-, package-0-die-,
  package-0-core-0), so every die zone is summed and watched.

The hardware test's oracle follows both rules: its zone check accepts die
zones, and its MSR reader groups Intel CPUs by package and die.

Tests (fake files): an Intel package of two dies is two counters, each read
on its own first CPU, both summed, with distinct rails, and a die whose device
is missing leaves no figure; an AMD package whose CPUs report different
die_ids is one counter; an Intel host with no die_id files, or all on die 0,
is read per package as before; the name rule over die names and near misses;
a fake two-die powercap tree sums both zones and is frozen when one die holds.
Unit tests pass on Linux x86_64 (default and cuda) and on macOS (the CI step,
its `ioreport liveness:` line printed); clippy is clean on macOS and on Linux
in the default build.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-04 21:20:05 -05:00

565 lines
25 KiB
Rust

//! Linux RAPL against the hardware: this host's powercap counters, read by
//! the meter and by an oracle the tests declare here.
//!
//! In a test binary of its own, so the RAPL probe behind `rapl_status()`
//! runs in this process, and the tests take one lock: each burns CPU and
//! reads machine-wide counters. The oracle reads `/sys/class/powercap`
//! itself — every zone's `name`, `energy_uj` and `max_energy_range_uj` — and
//! applies the sustained-advance rule to the top-level `package-<n>` (and
//! `package-<p>-die-<d>`) zones on its own. None of these tests passes without asserting: where the
//! counters cannot be read (no powercap; a CI container, whose
//! `/sys/devices/virtual/powercap` Docker masks), the status must say so and
//! agree with the oracle; where they are live, both must say live, and the
//! meter's figure over a CPU burn must sit inside the oracle's package-only
//! bracket and clearly below package+psys and package+core.
//!
//! Where powercap lists no package (a kernel without the powercap RAPL
//! driver), the meter reads each package's energy-status MSR, and so does the
//! oracle, on its own: the CPU's vendor and family, each package's first CPU,
//! the registers at their offsets in `/dev/cpu/<n>/msr`. Run as a user that
//! cannot open those files (no `CAP_SYS_RAWIO`), both must say unavailable;
//! as root, both live, with the meter's figure inside the oracle's MSR
//! package bracket.
#![cfg(target_os = "linux")]
use std::path::Path;
use std::sync::{Mutex, MutexGuard};
use std::time::{Duration, Instant};
use eoc_meter::{CounterStatus, JouleCounter, LinuxRaplCounter};
/// One test at a time: each burns CPU and reads machine-wide counters.
static SERIAL: Mutex<()> = Mutex::new(());
fn serial() -> MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
/// The tests' own reading of `/sys/class/powercap`: every zone listed, its
/// `name`, `energy_uj` and `max_energy_range_uj`, read here and nowhere else,
/// and the sustained-advance verdict over the package zones by the family's
/// rule. Nothing in it comes from the crate under test.
#[allow(dead_code)]
mod oracle {
use std::os::raw::{c_int, c_long};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
/// Where the kernel lists its powercap zones.
pub const POWERCAP: &str = "/sys/class/powercap";
#[repr(C)]
struct Timespec {
tv_sec: c_long,
tv_nsec: c_long,
}
unsafe extern "C" {
fn clock_gettime(clock_id: c_int, tp: *mut Timespec) -> c_int;
}
/// `CLOCK_THREAD_CPUTIME_ID` in Linux `<linux/time.h>`.
const CLOCK_THREAD_CPUTIME_ID: c_int = 3;
/// This thread's CPU time.
pub fn thread_cpu() -> Duration {
let mut ts = Timespec { tv_sec: 0, tv_nsec: 0 };
assert_eq!(unsafe { clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) }, 0, "clock_gettime");
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
}
/// Spin this thread until it has used `d` of its own CPU time, or `wall_cap`
/// of wall time has passed. Returns the CPU it got.
pub fn burn(d: Duration, wall_cap: Duration) -> Duration {
let (started, wall) = (thread_cpu(), Instant::now());
let mut acc: u64 = 0;
while thread_cpu() - started < d && wall.elapsed() < wall_cap {
for _ in 0..1_000 {
acc = std::hint::black_box(acc.wrapping_add(1));
}
}
thread_cpu() - started
}
/// One entry of the powercap directory.
#[derive(Debug, Clone)]
pub struct Zone {
/// The directory entry: `intel-rapl:0`, `intel-rapl:0:0`, `intel-rapl-mmio:0`, ...
pub dir: String,
pub name: Option<String>,
pub energy_uj: Option<u64>,
pub max_uj: Option<u64>,
}
fn text(p: PathBuf) -> Option<String> {
std::fs::read_to_string(p).ok().map(|s| s.trim().to_string())
}
fn number(p: PathBuf) -> Option<u64> {
text(p)?.parse().ok()
}
/// Every entry under `root`, in name order.
pub fn zones(root: &Path) -> Vec<Zone> {
let Ok(entries) = std::fs::read_dir(root) else { return Vec::new() };
let mut zones: Vec<Zone> = entries
.flatten()
.map(|e| {
let dir = e.file_name().to_string_lossy().into_owned();
let p = e.path();
Zone { name: text(p.join("name")), energy_uj: number(p.join("energy_uj")), max_uj: number(p.join("max_energy_range_uj")), dir }
})
.collect();
zones.sort_by(|a, b| a.dir.cmp(&b.dir));
zones
}
fn digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
/// A top-level RAPL zone: `intel-rapl:<n>`. Not a sub-zone, not MMIO.
pub fn top_level(z: &Zone) -> bool {
z.dir.strip_prefix("intel-rapl:").is_some_and(digits)
}
/// A package zone: top-level, named `package-<n>`, or `package-<p>-die-<d>`
/// (the kernel's zone for one die of a multi-die Intel package).
pub fn is_package(z: &Zone) -> bool {
let named = |n: &str| {
n.strip_prefix("package-").is_some_and(|rest| match rest.split_once("-die-") {
None => digits(rest),
Some((p, d)) => digits(p) && digits(d),
})
};
top_level(z) && z.name.as_deref().is_some_and(named)
}
/// The platform zone.
pub fn is_psys(z: &Zone) -> bool {
top_level(z) && z.name.as_deref() == Some("psys")
}
/// A package's `core` sub-zone (`intel-rapl:<n>:<m>` named `core`).
pub fn is_core(z: &Zone) -> bool {
z.dir.strip_prefix("intel-rapl:").is_some_and(|r| r.contains(':')) && z.name.as_deref() == Some("core")
}
/// One zone's energy between two reads, modulo its own range.
pub fn delta(e0: u64, e1: u64, max: Option<u64>) -> Option<u64> {
if e1 >= e0 {
Some(e1 - e0)
} else {
max.filter(|&m| m > 0)?.checked_sub(e0)?.checked_add(e1)
}
}
/// `energy_uj` of each of `dirs` under `root`, in order; `None` if any fails.
pub fn read(root: &Path, dirs: &[&Zone]) -> Option<Vec<u64>> {
dirs.iter().map(|z| number(root.join(&z.dir).join("energy_uj"))).collect()
}
/// The summed energy of `which` of `set` between two reads of `set`.
pub fn energy(set: &[&Zone], before: &[u64], after: &[u64], which: impl Fn(&Zone) -> bool) -> u64 {
set.iter()
.zip(before.iter().zip(after))
.filter(|(z, _)| which(z))
.map(|(z, (&b, &a))| delta(b, a, z.max_uj).expect("a zone fell with no readable range"))
.sum()
}
/// What the oracle concluded about the package counters under a root.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
/// No top-level zone's `energy_uj` reads (no powercap, a masked or
/// root-only tree).
Unreadable,
/// Top-level zones read, but none is a package.
NoPackage,
/// Every package read, but not every one kept advancing.
Frozen,
/// Every package advanced in at least five intervals, at least 80% of
/// them, over at least 300 ms.
Live,
}
/// The oracle's sustained-advance run over the package zones under `root`.
#[derive(Debug)]
pub struct Watch {
pub verdict: Verdict,
pub packages: Vec<String>,
pub intervals: u32,
pub advances: Vec<u32>,
pub window: Duration,
}
impl Watch {
/// The verdict and each package's advances, for the test's log line.
pub fn summary(&self) -> String {
let per: Vec<String> =
self.packages.iter().zip(&self.advances).map(|(p, a)| format!("{p} advanced {a}/{}", self.intervals)).collect();
format!("{:?} ({} over {} ms)", self.verdict, per.join(", "), self.window.as_millis())
}
}
/// Where powercap lists no package, the oracle's own read of each
/// energy-status MSR: the CPU's vendor and family from `/proc/cpuinfo`,
/// each counter's lowest-numbered CPU from its `physical_package_id` and,
/// on Intel, its `die_id` (the kernel's RAPL MSRs are die-scope on Intel
/// and package-scope on AMD and Hygon, `arch/x86/events/rapl.c`), and the
/// registers read at their offsets in `/dev/cpu/<n>/msr` (the `msr` module
/// and `CAP_SYS_RAWIO`).
pub mod msr {
use std::os::unix::fs::FileExt;
/// One energy-status MSR: a package's, or one die's on Intel.
#[derive(Debug, Clone, Copy)]
pub struct Package {
pub id: u32,
/// The die within the package (0 on AMD and Hygon, and where the
/// kernel lists no `die_id`).
pub die: u32,
pub cpu: u32,
pub register: u64,
/// 2^-`shift` J per count.
pub shift: u32,
}
fn register(cpu: u32, at: u64) -> Option<u64> {
let file = std::fs::File::open(format!("/dev/cpu/{cpu}/msr")).ok()?;
let mut raw = [0u8; 8];
file.read_exact_at(&mut raw, at).ok()?;
Some(u64::from_le_bytes(raw))
}
/// This host's counters, each read on its first CPU: per die on
/// Intel, per package on AMD and Hygon. `None` when the CPU's MSRs are
/// not Intel family 6's or AMD Zen's/Hygon's, or any counter's
/// power-unit MSR cannot be read or reads a zero unit.
pub fn packages() -> Option<Vec<Package>> {
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok()?;
let field = |key: &str| {
cpuinfo.lines().find_map(|l| l.split_once(':').filter(|(k, _)| k.trim() == key).map(|(_, v)| v.trim().to_string()))
};
let family: u32 = field("cpu family")?.parse().ok()?;
let (unit_at, energy_at, per_die) = match field("vendor_id")?.as_str() {
"GenuineIntel" if family == 6 => (0x606, 0x611, true),
"AuthenticAMD" | "HygonGenuine" if family >= 0x17 => (0xC001_0299, 0xC001_029B, false),
_ => return None,
};
let number = |p: std::path::PathBuf| std::fs::read_to_string(p).ok().and_then(|s| s.trim().parse::<u32>().ok());
let mut first = std::collections::BTreeMap::<(u32, u32), u32>::new();
for e in std::fs::read_dir("/sys/devices/system/cpu").ok()?.flatten() {
let name = e.file_name().to_string_lossy().into_owned();
let Some(cpu) = name.strip_prefix("cpu").and_then(|n| n.parse::<u32>().ok()) else { continue };
let Some(id) = number(e.path().join("topology/physical_package_id")) else { continue };
let die = if per_die { number(e.path().join("topology/die_id")).unwrap_or(0) } else { 0 };
let lowest = first.entry((id, die)).or_insert(cpu);
*lowest = (*lowest).min(cpu);
}
let mut packages = Vec::new();
for ((id, die), cpu) in first {
let shift = ((register(cpu, unit_at)? >> 8) & 0x1F) as u32;
if shift == 0 {
return None;
}
packages.push(Package { id, die, cpu, register: energy_at, shift });
}
(!packages.is_empty()).then_some(packages)
}
/// A count's energy, µJ, rounded down: the low 32 bits at 2^-`shift` J.
pub fn uj(p: &Package, raw: u64) -> u64 {
(((raw & 0xFFFF_FFFF) as u128 * 1_000_000) >> p.shift) as u64
}
/// The value a package's microjoule figure wraps at: 2^32 counts.
pub fn range(p: &Package) -> u64 {
(((1u128 << 32) * 1_000_000) >> p.shift) as u64
}
/// Every package's energy, µJ, in order; `None` if any read fails.
pub fn read(packages: &[Package]) -> Option<Vec<u64>> {
packages.iter().map(|p| register(p.cpu, p.register).map(|raw| uj(p, raw))).collect()
}
/// The packages' summed energy between two reads, each modulo 2^32
/// counts.
pub fn energy(packages: &[Package], before: &[u64], after: &[u64]) -> u64 {
packages
.iter()
.zip(before.iter().zip(after))
.map(|(p, (&b, &a))| super::delta(b, a, Some(range(p))).expect("an MSR count fell past its range"))
.sum()
}
}
/// Sample every package's `energy_uj` (or, where powercap lists no
/// package, every package's energy-status MSR), burning 25 ms of this
/// thread's CPU between samples (capped at 500 ms of wall time), until
/// every package has advanced in at least five intervals, at least 80% of
/// those observed, over at least 300 ms; or one second has passed with at
/// least five intervals; or five seconds have.
pub fn watch(root: &Path) -> Watch {
let all = zones(root);
let packages: Vec<&Zone> = all.iter().filter(|z| is_package(z)).collect();
if packages.is_empty() {
if let Some(msrs) = msr::packages() {
let names = msrs.iter().map(|p| format!("package-{}-die-{} (MSR, cpu {})", p.id, p.die, p.cpu)).collect();
return watch_reads(names, || msr::read(&msrs));
}
let readable = all.iter().any(|z| top_level(z) && z.energy_uj.is_some());
let verdict = if readable { Verdict::NoPackage } else { Verdict::Unreadable };
return Watch { verdict, packages: Vec::new(), intervals: 0, advances: Vec::new(), window: Duration::ZERO };
}
let names = packages.iter().map(|z| z.dir.clone()).collect();
watch_reads(names, || read(root, &packages))
}
/// The sustained-advance run of [`watch`] over the packages `read` reads,
/// in `names` order.
fn watch_reads(names: Vec<String>, read: impl Fn() -> Option<Vec<u64>>) -> Watch {
let none = |verdict| Watch { verdict, packages: names.clone(), intervals: 0, advances: Vec::new(), window: Duration::ZERO };
let started = Instant::now();
let Some(mut prev) = read() else { return none(Verdict::Unreadable) };
let mut advances = vec![0u32; names.len()];
let mut intervals = 0u32;
let live = |adv: &[u32], n: u32, w: Duration| {
w >= Duration::from_millis(300) && adv.iter().all(|&a| a >= 5 && u64::from(a) * 5 >= u64::from(n) * 4)
};
loop {
let el = started.elapsed();
if live(&advances, intervals, el) || el >= Duration::from_secs(5) || (el >= Duration::from_secs(1) && intervals >= 5) {
break;
}
burn(Duration::from_millis(25), Duration::from_millis(500));
if let Some(now) = read() {
intervals += 1;
for ((a, n), p) in advances.iter_mut().zip(&now).zip(&prev) {
*a += u32::from(n > p);
}
prev = now;
}
}
let window = started.elapsed();
let verdict = if live(&advances, intervals, window) { Verdict::Live } else { Verdict::Frozen };
Watch { verdict, packages: names, intervals, advances, window }
}
/// The zones under `root`, as one line for the log.
pub fn listing(root: &Path) -> String {
zones(root)
.iter()
.map(|z| {
let kind = if is_package(z) {
"package"
} else if is_psys(z) {
"psys"
} else if is_core(z) {
"core sub-zone"
} else if top_level(z) {
"top-level"
} else {
"not summed"
};
let e = z.energy_uj.map_or("unreadable".to_string(), |e| e.to_string());
let m = z.max_uj.map_or("-".to_string(), |m| m.to_string());
format!("{}={} [{kind}] energy_uj={e} max={m}", z.dir, z.name.as_deref().unwrap_or("?"))
})
.collect::<Vec<_>>()
.join("; ")
}
/// The CI container's view of the host's powercap directory: the same
/// entries, each a symlink into `/sys/devices/virtual/powercap`, which
/// Docker masks with an empty tmpfs, so every target is missing. Built
/// from this host's own listing; removed when dropped.
pub struct MaskedView(pub PathBuf);
impl MaskedView {
/// Build the masked view of this host's powercap entries in a
/// temporary directory named by `tag` and this process.
pub fn of_host(tag: &str) -> MaskedView {
let root = std::env::temp_dir().join(format!("{tag}-masked-powercap-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("create the masked view");
let masked = root.join("masked-devices-virtual-powercap");
for z in zones(Path::new(POWERCAP)) {
std::os::unix::fs::symlink(masked.join(&z.dir), root.join(&z.dir)).expect("symlink");
}
MaskedView(root)
}
}
impl Drop for MaskedView {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// A bracket of reads around the meter's two reads: every package, psys
/// and core zone read at four points, outer and inner on each side.
pub struct Bracket<'a> {
pub set: Vec<&'a Zone>,
pub reads: [Vec<u64>; 4],
}
impl Bracket<'_> {
/// The packages' energy over the outer bracket, in µJ: the most a
/// package-only figure read inside it can be.
pub fn package_outer(&self) -> u64 {
energy(&self.set, &self.reads[0], &self.reads[3], is_package)
}
/// The packages' energy over the inner bracket, in µJ: the least a
/// package-only figure read around it can be.
pub fn package_inner(&self) -> u64 {
energy(&self.set, &self.reads[1], &self.reads[2], is_package)
}
/// The psys zone's energy over the inner bracket, in µJ.
pub fn psys_inner(&self) -> u64 {
energy(&self.set, &self.reads[1], &self.reads[2], is_psys)
}
/// The core sub-zones' energy over the inner bracket, in µJ.
pub fn core_inner(&self) -> u64 {
energy(&self.set, &self.reads[1], &self.reads[2], is_core)
}
/// Whether the bracket read a zone of the kind `which` picks.
pub fn has(&self, which: impl Fn(&Zone) -> bool) -> bool {
self.set.iter().any(|z| which(z))
}
}
/// The family's rule on a figure `uj` the meter read between the
/// bracket's inner and outer reads: no less than the packages' energy
/// over the inner bracket, no more than over the outer one, and below
/// package+psys and package+core where those zones exist. A figure that
/// added a zone reads at least the packages' and that zone's energy over
/// the inner bracket, so `uj < inner + zone` is the double-count test; it
/// can only be put where the zone moved more over the burn than the
/// packages did in the gap between the outer and inner reads (else a
/// correct figure could reach it too), and the line says when it could
/// not. Returns the figures, for the log.
pub fn assert_package_only(b: &Bracket, uj: u64) -> String {
let (inner, outer, psys, core) = (b.package_inner(), b.package_outer(), b.psys_inner(), b.core_inner());
let gap = outer - inner;
let mut line = format!(
"meter {uj} µJ; oracle package-only inner {inner} outer {outer} µJ (gap {gap}); psys inner {psys} µJ; \
core inner {core} µJ"
);
assert!(inner > 0, "the packages did not advance over the burn: {line}");
assert!(inner <= uj && uj <= outer, "the figure is outside the package-only bracket: {line}");
for (zone, present, moved) in [("psys", b.has(is_psys), psys), ("core", b.has(is_core), core)] {
if !present {
continue;
}
assert!(moved > 0, "{zone} did not advance: {line}");
if moved > gap {
assert!(uj < inner + moved, "the figure reaches package+{zone}: {zone} was added: {line}");
line.push_str(&format!("; below package+{zone}"));
} else {
line.push_str(&format!("; {zone} moved less than the gap, so only the outer bound tests it"));
}
}
line
}
/// Every package, psys and core zone of `all` whose counter reads.
pub fn bracket_set(all: &[Zone]) -> Vec<&Zone> {
all.iter().filter(|z| (is_package(z) || is_psys(z) || is_core(z)) && z.energy_uj.is_some()).collect()
}
/// One oracle read of every zone of `set`; panics if one fails.
pub fn read_all(set: &[&Zone]) -> Vec<u64> {
read(Path::new(POWERCAP), set).expect("an oracle read of the bracket zones")
}
/// Read the bracket around `meter_read` (called twice) and `work`:
/// outer, meter, inner, work, inner, meter, outer.
pub fn around<'a, M>(set: Vec<&'a Zone>, mut meter_read: impl FnMut() -> M, work: impl FnOnce()) -> (Bracket<'a>, M, M) {
let o0 = read_all(&set);
let m0 = meter_read();
let o1 = read_all(&set);
work();
let o2 = read_all(&set);
let m1 = meter_read();
let o3 = read_all(&set);
(Bracket { set, reads: [o0, o1, o2, o3] }, m0, m1)
}
}
use oracle::Verdict;
/// The status agrees with the oracle's own verdict, and only a live status
/// constructs the counter and gets the RAPL rung selected: a live counter's
/// advance is what eoc's consumers grade `Measured`, so the gate is the grade.
#[test]
fn the_rapl_status_agrees_with_the_oracle_and_gates_the_rung() {
let _one = serial();
let status = eoc_meter::rapl_status();
let watch = oracle::watch(Path::new(oracle::POWERCAP));
let constructed = LinuxRaplCounter::try_detect().is_ok();
let selected = eoc_meter::detect().name();
eprintln!(
"rapl hardware: {}; oracle {}; status={status:?} constructed={constructed} detect()={selected}",
oracle::listing(Path::new(oracle::POWERCAP)),
watch.summary()
);
assert_eq!(status == CounterStatus::Live, watch.verdict == Verdict::Live, "the status disagrees with the oracle");
match watch.verdict {
Verdict::Unreadable | Verdict::NoPackage => assert_eq!(status, CounterStatus::Unavailable),
Verdict::Frozen => assert_eq!(status, CounterStatus::Frozen),
Verdict::Live => {}
}
assert_eq!(constructed, status == CounterStatus::Live, "construction disagrees with the status");
assert_eq!(selected == "rapl", status == CounterStatus::Live, "detect() picked {selected} under {status:?}");
}
/// Over a 300 ms one-thread CPU burn the counter reads the packages' energy:
/// no less than the oracle's inner package bracket, no more than its outer
/// one, and clearly below package+psys and package+core (neither the
/// platform zone nor a package's own sub-zone is added). Where RAPL is not
/// live, no counter is constructed and there is no figure to grade.
#[test]
fn a_cpu_burn_reads_inside_the_package_only_bracket() {
let _one = serial();
let Ok(counter) = LinuxRaplCounter::try_detect() else {
let watch = oracle::watch(Path::new(oracle::POWERCAP));
eprintln!("rapl burn: no counter; oracle {}", watch.summary());
assert_ne!(watch.verdict, Verdict::Live, "the oracle saw live packages, yet no counter was constructed");
assert_ne!(eoc_meter::rapl_status(), CounterStatus::Live);
return;
};
let all = oracle::zones(Path::new(oracle::POWERCAP));
if !all.iter().any(oracle::is_package) {
// No powercap package, so the live counter is over the packages'
// energy-status MSRs: bracket it with the oracle's own MSR reads.
let msrs = oracle::msr::packages().expect("the counter reads the MSRs, so the oracle's own read of them must too");
let read = || oracle::msr::read(&msrs).expect("an oracle read of the MSRs");
let wall = Instant::now();
let o0 = read();
let m0 = counter.read_microjoules().expect("a live counter reads");
let o1 = read();
let cpu = oracle::burn(Duration::from_millis(300), Duration::from_secs(10));
let o2 = read();
let m1 = counter.read_microjoules().expect("a live counter reads");
let o3 = read();
let (inner, outer, uj) = (oracle::msr::energy(&msrs, &o1, &o2), oracle::msr::energy(&msrs, &o0, &o3), m1 - m0);
let line = format!("meter {uj} µJ; oracle MSR package-only inner {inner} outer {outer} µJ ({} packages)", msrs.len());
eprintln!("rapl burn: {cpu:?} of thread CPU in {:?}; {line}", wall.elapsed());
assert!(inner > 0, "the packages did not advance over the burn: {line}");
assert!(inner <= uj && uj <= outer, "the figure is outside the package-only bracket: {line}");
assert!(cpu >= Duration::from_millis(300), "the burn got only {cpu:?} of CPU");
return;
}
let wall = Instant::now();
let mut cpu = Duration::ZERO;
let (b, m0, m1) = oracle::around(
oracle::bracket_set(&all),
|| counter.read_microjoules().expect("a live counter reads"),
|| cpu = oracle::burn(Duration::from_millis(300), Duration::from_secs(10)),
);
let uj = m1 - m0;
eprintln!("rapl burn: {cpu:?} of thread CPU in {:?}; {}", wall.elapsed(), oracle::assert_package_only(&b, uj));
assert!(cpu >= Duration::from_millis(300), "the burn got only {cpu:?} of CPU");
}