mesh-meter: hw-meter-apple meters a Mac with the SMC where IOReport is not live (stacked on #622, #630) #631

Merged
dcharlot merged 3 commits from mesh-meter/smc into main 2026-11-05 06:13:34 -05:00
3 changed files with 53 additions and 10 deletions

View file

@ -18,7 +18,9 @@ hw-meter = ["dep:jouleclaw-energy"]
# Convenience: enable the platform backend most likely to be present.
hw-meter-rapl = ["hw-meter", "jouleclaw-energy/rapl"]
hw-meter-nvml = ["hw-meter", "jouleclaw-energy/nvml"]
hw-meter-apple = ["hw-meter", "jouleclaw-energy/ioreport"]
# Apple: IOReport where its CPU rail is live, else the Mac's whole-machine power
# from the SMC (ModelBased; spans shorter than its 2 s floor are estimates).
hw-meter-apple = ["hw-meter", "jouleclaw-energy/ioreport", "jouleclaw-energy/smc"]
[lints]
workspace = true

View file

@ -12,12 +12,14 @@
//! * [`ManualMeter`] — caller-driven, for tests and replaying recorded spans.
//!
//! With the `hw-meter` feature, [`HwMeter`] bridges the family's meter (RAPL /
//! NVML / Apple Silicon IOReport, else the family's estimator), so a span carries
//! the provenance of whatever actually metered it: `HwShunt` from a hardware
//! counter that is live for the domain it reads (Apple IOReport included, where
//! its CPU energy rail advances under load), `ModelBased` from a vendor energy
//! model, `Estimator` where the host exposes no live counter — as on Apple Silicon
//! whose IOReport CPU rail is frozen (macOS 27.0.1).
//! NVML / Apple Silicon IOReport / a Mac's SMC system power, else the family's
//! estimator), so a span carries the provenance of whatever actually metered it:
//! `HwShunt` from a hardware counter that is live for the domain it reads (Apple
//! IOReport included, where its CPU energy rail advances under load),
//! `ModelBased` from a vendor energy model — with `hw-meter-apple`, the SMC's
//! whole-machine power integral on a Mac whose IOReport CPU rail is frozen (macOS
//! 27 on an M5 Max and an M3 Ultra) — and `Estimator` where the host exposes
//! neither.
//!
//! Also with `hw-meter`, [`ProcessMeter`] meters an engine that runs in this
//! process (a GGUF decode, in process) by this process's own CPU energy and its
@ -276,8 +278,11 @@ mod hw {
/// estimator) into the Mesh [`Meter`] interface. The active backend's
/// provenance is mapped through unchanged, so honesty is preserved across the
/// bridge. On Apple Silicon whose IOReport CPU energy rail does not advance
/// (macOS 27.0.1 on an M5 Max and an M3 Ultra), the active backend is the load
/// estimator and spans say `Estimator`.
/// (macOS 27 on an M5 Max and an M3 Ultra), the active backend is, with
/// `hw-meter-apple`, the SMC's whole-machine power integral: a span at least
/// its 2 s minimum window long says `ModelBased`, a shorter one `Estimator`.
/// Without the SMC (or a Mac that does not publish it) the backend is the load
/// estimator, and spans say `Estimator`.
///
/// If the backend fails a read, the bridge takes this crate's no-hardware path
/// from then on: an [`EstimatorMeter`], continuing from the last good reading,
@ -684,6 +689,39 @@ mod hw {
}
}
/// On this Mac, the bridge over the family's meter: where IOReport's CPU
/// rail is frozen and the SMC publishes `PSTR`, the SMC's whole-machine
/// integral, whose 2 s floor makes a 20 ms span an estimate and a span
/// past it a modeled figure of the whole machine. Where IOReport is live,
/// a hardware reading.
#[cfg(all(feature = "hw-meter-apple", target_os = "macos"))]
#[test]
fn on_a_mac_the_smc_models_spans_past_its_floor_and_shorter_ones_are_estimates() {
let m = HwMeter::detect();
let short = span(&m);
println!("mesh-meter on this Mac: meter={} floor={:?} 20 ms span {short:?}", m.name(), m.min_window());
match m.name() {
"apple-ioreport" => assert_eq!(short.provenance, Provenance::HwShunt),
"apple-smc" => {
assert_eq!(m.min_window(), Duration::from_secs(2));
assert_eq!((short.provenance, short.measured_uj), (Provenance::Estimator, None), "{short:?}");
assert!(short.estimated_uj > 0);
let s = Session::start(&m);
std::thread::sleep(Duration::from_millis(2_200));
let long = s.stop();
println!("mesh-meter on this Mac: 2.2 s span {long:?}");
assert_eq!((long.provenance, long.measured_uj), (Provenance::ModelBased, None), "{long:?}");
// A running Mac draws at least ~1 W: 2.2 s is over 2 J.
assert!(long.estimated_uj > 2_000_000, "{long:?}");
assert!(long.check_provenance().is_ok());
}
other => {
assert_eq!(other, EstimatorMeter::default().name(), "an unexpected Apple backend");
assert_eq!(short.provenance, Provenance::Estimator);
}
}
}
/// A backend that steps `STEP` µJ per read from `BASE`, over `domain`, at
/// `grade` — a live NVML counter is `(Gpu, HwShunt)`.
struct Stepping {

View file

@ -29,7 +29,10 @@
//! - `--features server,hw-meter` (or `hw-meter-rapl`, `-nvml`, `-apple`):
//! `mesh_meter::HwMeter::detect`, the machine's counter, a reading only
//! where a live one covers the whole request (a RAPL package on Linux) and
//! `mesh_meter::EstimatorMeter` otherwise. A machine counter reads
//! `mesh_meter::EstimatorMeter` otherwise. With `-apple` on a Mac whose
//! IOReport CPU rail is frozen it is the SMC's whole-machine power: a modeled
//! figure (`ModelBased`) for a serve at least its 2 s floor long, an estimate
//! for a shorter one, as this engine's are. A machine counter reads
//! everything the host does over the span, not this request alone.
//! - `--features server` alone: `mesh_meter::EstimatorMeter`, wall time × an
//! assumed draw, `Estimator`.