Some checks failed
smart-byte conformance gate / Validate conformance JSON files (pull_request) Successful in 11s
smart-byte conformance gate / Second implementation reproduces every vector (pull_request) Successful in 3s
smart-byte conformance gate / Spec changes require vector-pack updates (pull_request) Successful in 3s
smart-byte conformance gate / VERSIONS.md and README.md must exist (pull_request) Successful in 3s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m26s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 59s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m49s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m20s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m13s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m10s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m6s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 23s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 2m20s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 23s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 42s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 39s
wai formal verification (integer core) / kani (pull_request) Successful in 3m56s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 53s
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Successful in 1m4s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 25s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 2m33s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 21m57s
standards workspaces build + test / aac-rs (pull_request) Successful in 8m24s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 4m4s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 2m34s
standards workspaces build + test / map-rs (pull_request) Successful in 4m15s
standards workspaces build + test / mesh-rs (pull_request) Successful in 2m34s
standards workspaces build + test / openpay (pull_request) Successful in 15m42s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 1m25s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 5m21s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m2s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 2m58s
standards workspaces build + test / arl-rs (pull_request) Successful in 3m19s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 5m44s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 7m23s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 9m0s
standards workspaces build + test / sense-rs (pull_request) Successful in 6m7s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 7m49s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 1m0s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 4m51s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 56s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 2m22s
wai cross-architecture byte-equality / windows-x86_64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · windows (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · windows (pull_request) Has been cancelled
jouleclaw's public-surface audit failed #129 on seven helpers in jouleclaw-energy's rapl_hardware.rs that had no doc comment: the bracket's zone sums, the masked view, and the watch summary. Document them, and the same helpers in the other crates' copies of the module, so the copies stay alike. Co-Authored-By: Claude Opus 5.5 <[email protected]>
425 lines
18 KiB
Rust
425 lines
18 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>` 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.
|
|
|
|
#![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>`.
|
|
pub fn is_package(z: &Zone) -> bool {
|
|
top_level(z) && z.name.as_deref().and_then(|n| n.strip_prefix("package-")).is_some_and(digits)
|
|
}
|
|
|
|
/// 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`, 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();
|
|
let none = |verdict| Watch { verdict, packages: names.clone(), intervals: 0, advances: Vec::new(), window: Duration::ZERO };
|
|
if packages.is_empty() {
|
|
let readable = all.iter().any(|z| top_level(z) && z.energy_uj.is_some());
|
|
return none(if readable { Verdict::NoPackage } else { Verdict::Unreadable });
|
|
}
|
|
let started = Instant::now();
|
|
let Some(mut prev) = read(root, &packages) else { return none(Verdict::Unreadable) };
|
|
let mut advances = vec![0u32; packages.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(root, &packages) {
|
|
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));
|
|
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");
|
|
}
|