//! 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-`, or //! `package--die-` 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 ``. 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, pub energy_uj: Option, pub max_uj: Option, } fn text(p: PathBuf) -> Option { std::fs::read_to_string(p).ok().map(|s| s.trim().to_string()) } fn number(p: PathBuf) -> Option { text(p)?.parse().ok() } /// Every entry under `root`, in name order. pub fn zones(root: &Path) -> Vec { let Ok(entries) = std::fs::read_dir(root) else { return Vec::new() }; let mut zones: Vec = 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:`. 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-`, or `package--die-` /// (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::` 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) -> Option { 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> { 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, pub intervals: u32, pub advances: Vec, 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 = 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 = 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) -> 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, mut read: impl FnMut() -> Option>) -> 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 { 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> { 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::().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 { 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> { 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(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::>() .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; 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 { 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 { 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}]"); }