All checks were successful
energy meters on real hardware / RAPL and NVML on jetson-hub (pull_request) Successful in 38s
energy meters on real hardware / sink decode table · RAPL on jetson-hub (NVML surveyed) (pull_request) Successful in 2m45s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 7s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m1s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m13s
standards workspaces build + test / proof-rs (proof/conformance/v1/joulespec.toml, proof-conformance, proof/proof-rs, proof-rs, linux, true) (pull_request) Successful in 1m27s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, -p sandbox-tee --features amd-kds,intel-pcs, sandbox-rs, linux) (pull_request) Successful in 2m7s
energy meters on real hardware / sink decode table · per-task energy and the GPU rail on macOS (pull_request) Successful in 3m17s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m56s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 5m40s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m1s
standards workspaces build + test / joulecontract-rs (joulecontract/joulecontract-rs, joulecontract-rs, linux-arm64, true) (pull_request) Successful in 2m48s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 15m55s
Since sandbox-meter's RAPL probe falls back to the energy-status MSRs where powercap lists no package zone, everything built on it reads such a host: PowerMeter (`rapl-msr`), and sandbox-exec-microvm's RaplMeter, which is that probe and which sandbox-proof's S5 proof meters with. Three places still assumed powercap. - sandbox-tee's attested_efficiency example named only `rapl-powercap` as a CPU counter, so on an MSR host it attested the reading as `Estimator`. Both probes are the same hardware counter: `rapl-msr` now attests as CounterClass::Rapl and seals HwShunt, as `rapl-powercap` does. Its test accepts either probe for the class, and a new test pins every probe name to its class. - sandbox-proof's accelerator test read its oracle from powercap only, so where the meter read the MSRs the oracle had no package and the test panicked. The oracle (oracle_package_uj_at, independent of the crate) now reads the powercap package zones where any is listed (package-<n>, or package-<n>-die-<m>), and otherwise each RAPL domain's energy-status MSR on its first CPU: a die of a package on Intel, a package on AMD and Hygon, its 32-bit count at its own unit, wrapping at 2^32 counts. A new test drives it over fake files: two dies of one Intel package, an AMD package whose CPUs list several die_ids, a zero unit, and a powercap die zone preferred. - sandbox-exec-microvm's rapl_hardware oracle watched only powercap, so where RaplMeter opened on the MSRs the oracle called the host Unreadable; and the MSR meter's test bracketed its figure with powercap zones a host without powercap does not have. The oracle now watches and brackets with its own MSR reads there, scoped the same way, and accepts die zones as packages. RaplMeter's docs say what it reads. On two bare-metal servers with no powercap zone (a one-socket Xeon E-2278G, two Xeon Silver 4215R sockets), as root: the accelerator span read rapl-msr at a ratio of 1.000 and 0.999 to the oracle around the call; both microvm hardware tests sat inside the oracle's MSR bracket (RaplMeter 8.9 J and 23.5 J over a 300 ms burn); the example attested `rapl` and sealed HwShunt. As an unprivileged user every counter is Unavailable, the figures are estimates, and every test passes; as that user with the msr group and CAP_SYS_RAWIO (systemd-run), the results match root's. Linux x86_64: sandbox-proof, sandbox-tee (default and amd-kds,intel-pcs) and sandbox-exec-microvm tests pass, `cargo check --workspace --all-targets` is clean, clippy -D warnings is clean for the three crates. macOS: the CI step for sandbox-proof passes with its grep, sandbox-tee passes, clippy is clean for sandbox-meter, sandbox-proof, sandbox-tee and sandbox-exec-microvm. Co-Authored-By: Claude Opus 5.5 <[email protected]>
627 lines
27 KiB
Rust
627 lines
27 KiB
Rust
//! The S5 executor's CPU energy meters against the hardware: RAPL through
|
|
//! powercap (`RaplMeter`, sandbox-meter's package counters) and through the
|
|
//! MSRs (`MsrRaplMeter`), read beside an oracle the tests declare here.
|
|
//!
|
|
//! In a test binary of its own, so the probes run in this process, and the
|
|
//! tests take one lock: each burns CPU and reads machine-wide counters. The
|
|
//! oracle reads `/sys/class/powercap` itself and applies the sustained-
|
|
//! advance rule to the top-level package zones (`package-<n>`, or
|
|
//! `package-<n>-die-<m>` on a multi-die package) on its own. Where powercap
|
|
//! lists no package (a kernel without the powercap RAPL driver), it reads the
|
|
//! RAPL energy-status MSRs itself (`oracle::msr`), each domain on its first
|
|
//! CPU: a die of a package on Intel, a package on AMD and Hygon — the domains
|
|
//! `RaplMeter` reads there too, through sandbox-meter's probe. None of these
|
|
//! tests passes without asserting: where a counter cannot be read the meter
|
|
//! must say so and the oracle agree; where it is live, the meter's figure over
|
|
//! a CPU burn must sit inside the oracle's package-only bracket.
|
|
|
|
#![cfg(target_os = "linux")]
|
|
|
|
use std::path::Path;
|
|
use std::sync::{Mutex, MutexGuard};
|
|
use std::time::Duration;
|
|
|
|
use sandbox_exec_microvm::power::{MsrRaplMeter, PowerMeter, RaplMeter};
|
|
|
|
/// 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-<n>-die-<m>`
|
|
/// (one zone per die of a multi-die Intel package).
|
|
pub fn is_package(z: &Zone) -> bool {
|
|
top_level(z)
|
|
&& z.name.as_deref().and_then(|n| n.strip_prefix("package-")).is_some_and(|n| match n.split_once("-die-") {
|
|
Some((package, die)) => digits(package) && digits(die),
|
|
None => digits(n),
|
|
})
|
|
}
|
|
|
|
/// 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())
|
|
}
|
|
}
|
|
|
|
/// Sample every package's `energy_uj` under `root`, 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();
|
|
let names: Vec<String> = packages.iter().map(|z| z.dir.clone()).collect();
|
|
if packages.is_empty() {
|
|
let readable = all.iter().any(|z| top_level(z) && z.energy_uj.is_some());
|
|
return Watch::none(if readable { Verdict::NoPackage } else { Verdict::Unreadable }, names);
|
|
}
|
|
watch_with(names, || read(root, &packages))
|
|
}
|
|
|
|
/// Whether the host's powercap lists a package zone.
|
|
pub fn powercap_lists_a_package() -> bool {
|
|
zones(Path::new(POWERCAP)).iter().any(is_package)
|
|
}
|
|
|
|
/// [`watch`] over this host: its powercap package zones, or, where powercap
|
|
/// lists none, its RAPL MSRs ([`msr::domains`]), sampled the same way.
|
|
pub fn watch_host() -> Watch {
|
|
if powercap_lists_a_package() {
|
|
return watch(Path::new(POWERCAP));
|
|
}
|
|
match msr::domains() {
|
|
Some(domains) => {
|
|
let names = domains.iter().map(|d| d.name.clone()).collect();
|
|
watch_with(names, || msr::read_all(&domains))
|
|
}
|
|
None => watch(Path::new(POWERCAP)),
|
|
}
|
|
}
|
|
|
|
impl Watch {
|
|
fn none(verdict: Verdict, packages: Vec<String>) -> Watch {
|
|
Watch { verdict, packages, intervals: 0, advances: Vec::new(), window: Duration::ZERO }
|
|
}
|
|
}
|
|
|
|
/// The sustained-advance run over the counters `read` returns, one per name.
|
|
fn watch_with(names: Vec<String>, mut read: impl FnMut() -> Option<Vec<u64>>) -> Watch {
|
|
let started = Instant::now();
|
|
let Some(mut prev) = read() else { return Watch::none(Verdict::Unreadable, names) };
|
|
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 oracle's own reading of the RAPL energy-status MSRs, for a host
|
|
/// whose powercap lists no package: each domain's count through the `msr`
|
|
/// driver's device file of its first CPU. A domain is a die of a package on
|
|
/// Intel (its RAPL MSRs are die-scope) and a package on AMD and Hygon (a
|
|
/// per-package counter).
|
|
pub mod msr {
|
|
use super::number;
|
|
use std::os::unix::fs::FileExt;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// One RAPL domain's energy-status MSR.
|
|
pub struct Domain {
|
|
pub name: String,
|
|
device: PathBuf,
|
|
register: u64,
|
|
shift: u32,
|
|
}
|
|
|
|
fn read(device: &Path, register: u64) -> Option<u64> {
|
|
let file = std::fs::File::open(device).ok()?;
|
|
let mut raw = [0u8; 8];
|
|
file.read_exact_at(&mut raw, register).ok()?;
|
|
Some(u64::from_le_bytes(raw))
|
|
}
|
|
|
|
/// Every domain whose power-unit and energy-status MSRs read with a
|
|
/// non-zero unit; `None` when the CPU's MSRs are not known here, no
|
|
/// topology is listed, or any domain cannot be read.
|
|
pub fn domains() -> Option<Vec<Domain>> {
|
|
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()))
|
|
};
|
|
let family: u32 = field("cpu family")?.parse().ok()?;
|
|
let (unit, energy, per_die) = match field("vendor_id")? {
|
|
"GenuineIntel" if family == 6 => (0x606, 0x611, true),
|
|
"AuthenticAMD" | "HygonGenuine" if family >= 0x17 => (0xC001_0299, 0xC001_029B, false),
|
|
_ => return None,
|
|
};
|
|
let mut first = std::collections::BTreeMap::<(u64, u64), u32>::new();
|
|
for entry in std::fs::read_dir("/sys/devices/system/cpu").ok()?.flatten() {
|
|
let Some(cpu) = entry.file_name().to_str().and_then(|n| n.strip_prefix("cpu")?.parse::<u32>().ok()) else {
|
|
continue;
|
|
};
|
|
let topology = entry.path().join("topology");
|
|
let Some(package) = number(topology.join("physical_package_id")) else { continue };
|
|
let die = if per_die { number(topology.join("die_id")).unwrap_or(0) } else { 0 };
|
|
let lowest = first.entry((package, die)).or_insert(cpu);
|
|
*lowest = (*lowest).min(cpu);
|
|
}
|
|
if first.is_empty() {
|
|
return None;
|
|
}
|
|
first
|
|
.into_iter()
|
|
.map(|((package, die), cpu)| {
|
|
let device = PathBuf::from(format!("/dev/cpu/{cpu}/msr"));
|
|
let shift = ((read(&device, unit)? >> 8) & 0x1F) as u32;
|
|
if shift == 0 {
|
|
return None;
|
|
}
|
|
read(&device, energy)?;
|
|
Some(Domain { name: format!("msr package-{package}-die-{die} (cpu {cpu})"), device, register: energy, shift })
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// One domain's count, in µJ.
|
|
fn uj(d: &Domain) -> Option<u64> {
|
|
read(&d.device, d.register).map(|raw| ((u128::from(raw & 0xFFFF_FFFF) * 1_000_000) >> d.shift) as u64)
|
|
}
|
|
|
|
/// Every domain's count, in µJ; `None` if one fails.
|
|
pub fn read_all(domains: &[Domain]) -> Option<Vec<u64>> {
|
|
domains.iter().map(uj).collect()
|
|
}
|
|
|
|
/// The domains' summed energy between two reads, each modulo its 2^32 counts.
|
|
pub fn energy(domains: &[Domain], before: &[u64], after: &[u64]) -> u64 {
|
|
domains
|
|
.iter()
|
|
.zip(before.iter().zip(after))
|
|
.map(|(d, (&b, &a))| {
|
|
let range = (((1u128 << 32) * 1_000_000) >> d.shift) as u64;
|
|
super::delta(b, a, Some(range)).expect("an MSR count above its range")
|
|
})
|
|
.sum()
|
|
}
|
|
|
|
/// Read the domains outer and inner around `meter_read` (called twice)
|
|
/// and `work`: the domains' energy over the inner and the outer window,
|
|
/// and the meter's two readings.
|
|
pub fn around<M>(domains: &[Domain], mut meter_read: impl FnMut() -> M, work: impl FnOnce()) -> (u64, u64, M, M) {
|
|
let read = || read_all(domains).expect("an oracle MSR read");
|
|
let o0 = read();
|
|
let m0 = meter_read();
|
|
let o1 = read();
|
|
work();
|
|
let o2 = read();
|
|
let m1 = meter_read();
|
|
let o3 = read();
|
|
(energy(domains, &o1, &o2), energy(domains, &o0, &o3), m0, m1)
|
|
}
|
|
}
|
|
|
|
/// 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 powercap meter opens exactly where the oracle finds every package
|
|
/// live, and its figure over a 300 ms burn sits inside the oracle's
|
|
/// package-only bracket and clearly below package+psys and package+core.
|
|
#[test]
|
|
fn the_powercap_meter_opens_where_the_packages_are_live_and_reads_them() {
|
|
let _one = serial();
|
|
let meter = RaplMeter::host();
|
|
let watch = oracle::watch_host();
|
|
eprintln!(
|
|
"rapl hardware: {}; oracle {}; RaplMeter unavailable={:?}",
|
|
oracle::listing(Path::new(oracle::POWERCAP)),
|
|
watch.summary(),
|
|
meter.unavailable()
|
|
);
|
|
assert_eq!(meter.unavailable().is_none(), watch.verdict == Verdict::Live, "the meter disagrees with the oracle");
|
|
if meter.unavailable().is_some() {
|
|
assert_eq!(meter.sample_uj(), None, "a meter that did not open sampled");
|
|
return;
|
|
}
|
|
if !oracle::powercap_lists_a_package() {
|
|
// No powercap package: the meter reads the MSRs (sandbox-meter's probe
|
|
// falls back to them), and so does the oracle.
|
|
let domains = oracle::msr::domains().expect("the meter reads the MSRs, so the oracle can");
|
|
let mut cpu = Duration::ZERO;
|
|
let (inner, outer, m0, m1) = oracle::msr::around(
|
|
&domains,
|
|
|| meter.sample_uj().expect("an open meter samples"),
|
|
|| cpu = oracle::burn(Duration::from_millis(300), Duration::from_secs(10)),
|
|
);
|
|
let uj = m1 - m0;
|
|
let names: Vec<&str> = domains.iter().map(|d| d.name.as_str()).collect();
|
|
eprintln!("rapl burn (MSR {names:?}): {cpu:?} of thread CPU; meter {uj} µJ; oracle inner {inner} outer {outer} µJ");
|
|
assert!(inner > 0, "the domains did not advance over the burn");
|
|
// One count of the energy unit (2^-14 J, ~61 µJ) per domain either side.
|
|
let slack = 123 * domains.len() as u64;
|
|
assert!(inner.saturating_sub(slack) <= uj && uj <= outer + slack, "{uj} µJ outside [{inner}, {outer}]");
|
|
assert!(cpu >= Duration::from_millis(300), "the burn got only {cpu:?} of CPU");
|
|
return;
|
|
}
|
|
let all = oracle::zones(Path::new(oracle::POWERCAP));
|
|
let mut cpu = Duration::ZERO;
|
|
let (b, m0, m1) = oracle::around(
|
|
oracle::bracket_set(&all),
|
|
|| meter.sample_uj().expect("an open meter samples"),
|
|
|| cpu = oracle::burn(Duration::from_millis(300), Duration::from_secs(10)),
|
|
);
|
|
eprintln!("rapl burn: {cpu:?} of thread CPU; {}", oracle::assert_package_only(&b, m1 - m0));
|
|
assert!(cpu >= Duration::from_millis(300), "the burn got only {cpu:?} of CPU");
|
|
}
|
|
|
|
/// `MSR_PKG_ENERGY_STATUS` read directly, the oracle's own way: `pread` of
|
|
/// 8 bytes at offset 0x611 of `/dev/cpu/0/msr`. `None` where it cannot be
|
|
/// read (no `msr` module, or not root: `CAP_SYS_RAWIO`).
|
|
fn oracle_msr_reads() -> Option<u64> {
|
|
use std::os::unix::fs::FileExt;
|
|
let f = std::fs::File::open("/dev/cpu/0/msr").ok()?;
|
|
let mut buf = [0u8; 8];
|
|
f.read_exact_at(&mut buf, 0x611).ok()?;
|
|
Some(u64::from_le_bytes(buf))
|
|
}
|
|
|
|
/// The MSR meter opens only where the MSRs read: where the oracle cannot read
|
|
/// `MSR_PKG_ENERGY_STATUS` (an unprivileged process), the meter says why and
|
|
/// samples nothing; where it can, the meter opens and its figure over a 300 ms
|
|
/// burn sits inside the powercap packages' bracket, to a count's rounding.
|
|
#[test]
|
|
fn the_msr_meter_opens_only_where_the_msrs_read() {
|
|
let _one = serial();
|
|
let msr = oracle_msr_reads();
|
|
let meter = MsrRaplMeter::host();
|
|
eprintln!(
|
|
"rapl msr hardware: /dev/cpu/0/msr {:?}; oracle MSR_PKG_ENERGY_STATUS {msr:?}; MsrRaplMeter {:?}",
|
|
std::fs::metadata("/dev/cpu/0/msr").map(|m| m.len()).map_err(|e| e.kind()),
|
|
meter.as_ref().map(|m| m.domains()).map_err(String::as_str)
|
|
);
|
|
let Some(_) = msr else {
|
|
assert!(meter.is_err(), "the MSR meter opened where the oracle cannot read the MSR");
|
|
return;
|
|
};
|
|
let meter = meter.expect("the MSRs read, yet the MSR meter did not open");
|
|
if !oracle::powercap_lists_a_package() {
|
|
// No powercap package to bracket with (the host this meter is for):
|
|
// the oracle's own MSR reads bracket it instead.
|
|
let domains = oracle::msr::domains().expect("the MSRs read, so the oracle's domains do");
|
|
let (inner, outer, m0, m1) = oracle::msr::around(
|
|
&domains,
|
|
|| meter.sample_uj().expect("an open MSR meter samples"),
|
|
|| {
|
|
oracle::burn(Duration::from_millis(300), Duration::from_secs(10));
|
|
},
|
|
);
|
|
let uj = m1 - m0;
|
|
eprintln!("rapl msr burn (no powercap): meter {uj} µJ; oracle MSR inner {inner} outer {outer} µJ");
|
|
assert!(inner > 0, "the domains did not advance over the burn");
|
|
let slack = 123 * domains.len() as u64;
|
|
assert!(inner.saturating_sub(slack) <= uj && uj <= outer + slack, "{uj} µJ outside [{inner}, {outer}]");
|
|
return;
|
|
}
|
|
let all = oracle::zones(Path::new(oracle::POWERCAP));
|
|
let (b, m0, m1) = oracle::around(
|
|
oracle::bracket_set(&all),
|
|
|| meter.sample_uj().expect("an open MSR meter samples"),
|
|
|| {
|
|
oracle::burn(Duration::from_millis(300), Duration::from_secs(10));
|
|
},
|
|
);
|
|
let uj = m1 - m0;
|
|
let (inner, outer) = (b.package_inner(), b.package_outer());
|
|
eprintln!("rapl msr burn: meter {uj} µJ; oracle package-only inner {inner} outer {outer} µJ");
|
|
// One count of the energy unit (2^-14 J, ~61 µJ) either side.
|
|
assert!(inner.saturating_sub(123) <= uj && uj <= outer + 123, "{uj} µJ outside [{inner}, {outer}]");
|
|
}
|