jouleclaw-energy: a Mac's whole-machine power from the SMC (EnergyDomain::System, ModelBased) #622

Merged
dcharlot merged 1 commit from jouleclaw-energy/smc-system into main 2026-11-05 04:30:59 -05:00
10 changed files with 903 additions and 30 deletions

View file

@ -635,6 +635,22 @@ jobs:
# tests/process_gpu.rs prints this once its 1.5 s GPU burn was checked against the oracle.
grep -q 'process-gpu: oracle-checked:' "$log"
# The SMC whole-machine meter, `cfg(all(feature = "smc", target_os = "macos"))`,
# alone and beside IOReport (which changes what `detect_meter` picks). Its
# hardware test prints `apple-smc: measured` only once it has read a figure
# from the real SMC, so the grep fails if the runner had no `PSTR` or a cfg
# left the test unrun.
- name: jouleclaw-energy (smc, feature AND platform gated)
shell: bash
working-directory: jouleclaw/jouleclaw-rs
run: |
set -o pipefail
for features in smc ioreport,smc; do
log=$(mktemp)
cargo test -p jouleclaw-energy --features "$features" -- --nocapture 2>&1 | tee "$log"
grep -q 'apple-smc: measured' "$log"
done
- name: sandbox-meter
shell: bash
working-directory: sandbox/sandbox-rs
@ -765,6 +781,8 @@ jobs:
lint proof/proof-rs -p proof-cascade -p proof-solvers
lint eoc/eoc-rs -p eoc-meter
lint jouleclaw/jouleclaw-rs -p jouleclaw-energy --features ioreport
lint jouleclaw/jouleclaw-rs -p jouleclaw-energy --features smc
lint jouleclaw/jouleclaw-rs -p jouleclaw-energy --features ioreport,smc
# And the DEFAULT build. Linting only the featured one hid a real
# clippy error for as long as it existed: every hardware meter in
# `detect_meter` is behind a `cfg`, so the default build is a

View file

@ -71,7 +71,7 @@ how the value was obtained:
| Provenance | Meaning |
|----------------|--------------------------------------------------------|
| `HwShunt` | Hardware reading: a shunt / coulomb counter (Jetson INA3221), or a vendor energy counter (RAPL MSR, NVML cumulative-energy counter, Apple IOReport) while it is live for the domain it claims (below) |
| `ModelBased` | Vendor-provided estimate from freq / voltage / utilisation (ROCm SMI, NVML `power.draw`) |
| `ModelBased` | Vendor-provided estimate from freq / voltage / utilisation (ROCm SMI, NVML `power.draw`), or a vendor's sampled power integrated over time (Apple SMC `PSTR`, whole machine) |
| `Estimator` | JouleClaw static cost model from arch × precision × batch tables |
A vendor energy counter's reading is a real reading. What separates

View file

@ -16,7 +16,8 @@ tracing = { workspace = true, optional = true }
nvml-wrapper = { workspace = true, optional = true }
# `libc`: the IOReport backend's CoreFoundation types and thread clock on macOS,
# and the liveness probe's thread CPU clock on every Unix (RAPL on Linux).
# the SMC backend's Mach types, and the liveness probe's thread CPU clock on
# every Unix (RAPL on Linux).
[target.'cfg(unix)'.dependencies]
libc = "0.2"
@ -31,6 +32,7 @@ nvml = ["dep:nvml-wrapper", "dep:tracing"] # NVIDIA discrete — NVML cumul
ioreport = [] # Apple Silicon M-series — real IOReport energy counter (HwShunt while every CPU rail is live), load-avg estimator fallback; and the per-process GPU counter (GPU rail × this process's GPU-time share)
ina3221 = [] # NVIDIA Jetson (Orin / Thor) — sysfs i2c shunt reader
hwmon = [] # Linux generic — for AMD Strix Halo / Ryzen AI Max iGPU + others
smc = [] # Mac — the SMC's whole-machine power (`PSTR`) integrated over time (ModelBased, domain System); a sampler thread per process
# The per-process GPU counter's hardware test loads the GPU itself (this crate
# holds no Metal; its consumers supply the load). Same Metal bindings as

View file

@ -11,8 +11,9 @@ is its provenance — which counter, which domain, and whether it was live — n
its accuracy. A counter that is present but not live (frozen, or republished in
lumps) gives no `HwShunt` figure: the meter falls back to its estimator.
The goal is that on real hardware a reading is **never** an `Estimator`. It is
not met on Apple Silicon under macOS 27.0.1 (below). The
The goal is that on real hardware a reading is **never** an `Estimator`.
IOReport does not meet it on Apple Silicon under macOS 27 (below). Built with
`smc`, such a Mac is metered as a whole machine instead, `ModelBased` (below). The
[`energy_probe`](examples/energy_probe.rs) example reads the best available
counter for its build features, integrates over a ~2 s CPU-churn window, and
prints the measured microjoules with provenance:
@ -21,6 +22,7 @@ prints the measured microjoules with provenance:
cargo run -p jouleclaw-energy --release --example energy_probe --features nvml # NVIDIA (Linux/Windows)
cargo run -p jouleclaw-energy --release --example energy_probe --features rapl # x86 (Linux)
cargo run -p jouleclaw-energy --release --example energy_probe --features ioreport # Apple Silicon
cargo run -p jouleclaw-energy --release --example energy_probe --features smc # Mac, whole machine
```
## Verified on real hardware
@ -31,6 +33,8 @@ cargo run -p jouleclaw-energy --release --example energy_probe --features iorepo
| Windows 11 + GeForce RTX 2060 | NVML (`nvml.dll`) | GPU | **HwShunt** | 18.8 J · 9.4 W |
| Linux x86 + Tesla T4 (AWS g4dn.metal) | NVML (`libnvidia-ml`) | GPU | **HwShunt** | 148.4 J · 71.8 W |
| Linux x86, Xeon Platinum 8259CL (AWS g4dn.metal) | RAPL powercap | CPU package | **HwShunt** | 154.2 J · 77.1 W (1 µJ res) |
| Mac Studio, M3 Ultra, macOS 27.2 | SMC `PSTR`, integrated | whole machine (`System`) | **ModelBased** | 299.9 J · 120.0 W (2.5 s) |
| MacBook Pro, M5 Max, macOS 27.2, on AC | SMC `PSTR`, integrated | whole machine (`System`) | **ModelBased** | 160.9 J · 64.4 W (2.5 s) |
Notes:
@ -145,11 +149,46 @@ checked against the test's own read on each span checked against the oracle (the
span included. On a shared host, the 1.5 s burn gives a figure or `GpuShared`,
depending on what else runs on the GPU.
## A Mac as a whole machine (SMC `PSTR`, unprivileged)
The SMC publishes the machine's total power as the key `PSTR`, and any process
reads it through the `AppleSMCKeysEndpoint` IOKit user client, with no root.
[`apple_smc`](src/apple_smc.rs) (feature `smc`) integrates it over time:
| | |
|---|---|
| Domain | `System`: everything the machine's DC input feeds (SoC, memory, storage, fans, the rest of the board), not the power supply's AC-to-DC loss |
| Grade | **ModelBased**: a sampled power value integrated over time, like NVML's `power.draw` integral, never `HwShunt` |
| Sampling | one thread per process reads `PSTR` every 100 ms and integrates the value it holds, so a figure does not depend on how often its caller reads |
| Checks | `PSTR` must be a 4-byte float and be seen to refresh before the counter is available; samples at 0 W or less, above 1.5 kW, or not a number are discarded; a value held 10 s gives no figure |
| Minimum window | 2 s |
Measured on macOS 27.2:
- **Refresh.** `PSTR` changes about once a second on an M3 Ultra Mac Studio and
an M5 Max MacBook Pro (median gap between changes 1.0 s on both), and trails a
step in CPU load by about one refresh. Hence the 2 s minimum window.
- **Coverage.** On the Mac Studio `PSTR` equals the DC input, `PDTR`. On the
MacBook Pro on AC with the battery not charging, `PSTR` read 51.7 W against
54.9 W at the DC input: it is the system's draw, not the input's.
- **Not established:** whether each value averages the second before it or
samples one point. Other work on the dev Mac swings it between about 35 and 64 W,
which hides a 4-core burst of 200 ms. The `ModelBased` grade and the 2 s window
hold either way.
`detect_meter` puts it after a live IOReport counter, a hardware reading over a
narrower domain, and before the load-average estimator, which reads no power. On
both Macs above IOReport's CPU rails are frozen, so with `smc` the host meter is
the SMC: a figure for the whole machine, which holds the work and everything
else the machine powered over the window, so it can read high for the work but
never low.
## Not yet a hardware reading
- **Apple Silicon host-wide CPU energy on macOS 27.0.1**, for an unprivileged
process — the IOReport CPU rails are frozen (above), so the load-avg estimator
meters work outside this process.
meters work outside this process. Built with `smc`, the whole machine meters
it instead, `ModelBased` (above): a figure for the machine, not for the CPU.
- **Apple Neural Engine energy**, unprivileged: `ANE0` is frozen and ANE work
runs in `aned`; the `H1xANELoadBalancer` clients carry no time or energy.
- **x86 CPU energy on Windows** — RAPL MSRs need a kernel driver (e.g. WinRing0)

View file

@ -15,6 +15,10 @@
//! # load-avg estimator, with the reason printed):
//! cargo run -p jouleclaw-energy --release --example energy_probe --features ioreport
//!
//! # Mac, whole machine (the SMC's `PSTR` integrated over time, ModelBased;
//! # with `ioreport` too, after a live IOReport counter):
//! cargo run -p jouleclaw-energy --release --example energy_probe --features smc
//!
//! It samples the counter across a ~2 s window (driving a small CPU workload so
//! the package/SoC is not fully idle) and prints the integrated microjoules. For
//! the integrating counters (NVML) the repeated reads during the window are what
@ -65,6 +69,20 @@ fn pick_counter() -> Option<(&'static str, Box<dyn EnergyCounter>)> {
return Some(("apple-ioreport (Energy Model)", Box::new(c)));
}
println!("ioreport : declined ({:?})", c.status());
}
#[cfg(all(feature = "smc", target_os = "macos"))]
{
// The whole machine's power from the SMC, integrated over time: a vendor
// power reading, so ModelBased, over the System domain.
let c = jouleclaw_energy::apple_smc::SmcSystemCounter::new();
if c.is_available() {
println!("smc : PSTR {:.1} W now", c.watts().unwrap_or(0.0));
return Some(("apple-smc (SMC PSTR, whole machine)", Box::new(c)));
}
println!("smc : unavailable ({})", c.unavailable_reason().unwrap_or("?"));
}
#[cfg(all(feature = "ioreport", target_os = "macos"))]
{
let c = jouleclaw_energy::apple::AppleSiliconMeter::new();
if c.is_available() {
return Some(("apple (load-avg estimator)", Box::new(c)));
@ -93,7 +111,7 @@ fn main() {
let Some((label, counter)) = pick_counter() else {
eprintln!(
"no hardware energy counter available on this host/build.\n\
rebuild with --features nvml|rapl|ioreport for this platform."
rebuild with --features nvml|rapl|ioreport|smc for this platform."
);
std::process::exit(1);
};
@ -114,9 +132,11 @@ fn main() {
}
};
// Integrate over a ~2 s window: churn the CPU and re-read every ~200 ms so
// the integrating counters accumulate the wall-power integral.
let window = Duration::from_secs(2);
// Integrate over a ~2 s window, or the counter's minimum window if longer
// (plus a margin): churn the CPU and re-read every ~200 ms so the
// integrating counters accumulate the wall-power integral.
let floor = Duration::from_nanos(counter.min_window_ns());
let window = Duration::from_secs(2).max(floor + Duration::from_millis(500));
let start = Instant::now();
let mut checksum: u64 = 0;
while start.elapsed() < window {

View file

@ -3596,13 +3596,15 @@ mod tests {
/// A declined counter yields no number at all, and the fallback that runs
/// in its place carries its own grade end to end: `detect_meter` hands out
/// the load-average estimator, and its readings say `Estimator` — never a
/// hardware-graded figure from a frozen rail. And it meters work that ran:
/// 150 ms of CPU work, read through `detect_meter`, is a figure larger than
/// one quantum of the meter, at the fallback's grade — not a zero, and not
/// a floor.
/// the SMC's whole-machine integral where the `smc` feature is on and
/// `PSTR` is live (its readings say `ModelBased`), and the load-average
/// estimator otherwise (`Estimator`) — never a hardware-graded figure from
/// a frozen rail. And it meters work that ran: 150 ms of CPU work, or the
/// meter's minimum window if longer, read through `detect_meter`, is a
/// figure larger than one quantum of the meter, at the fallback's grade —
/// not a zero, and not a floor.
#[test]
fn a_declined_counter_yields_no_reading_and_the_fallback_says_estimator() {
fn a_declined_counter_yields_no_reading_and_the_fallback_carries_its_grade() {
let counter = AppleIoReportCounter::new();
let composite = crate::meter::detect_meter();
let active = composite.active().expect("detect_meter always has a meter");
@ -3616,11 +3618,22 @@ mod tests {
matches!(counter.read(), Err(EnergyError::NoCounter(EnergyDomain::SocTotal))),
"a declined IOReport counter must not produce a reading"
);
// The load-average estimator on Apple Silicon; the static one elsewhere.
let fallback = if cfg!(target_arch = "aarch64") { "apple-silicon" } else { "estimation" };
// The SMC's whole-machine integral where it is built and live; else
// the load-average estimator on Apple Silicon, the static one elsewhere.
#[cfg(feature = "smc")]
let smc = crate::apple_smc::SmcSystemCounter::new().is_available();
#[cfg(not(feature = "smc"))]
let smc = false;
let (fallback, grade) = if smc {
("apple-smc", Provenance::ModelBased)
} else if cfg!(target_arch = "aarch64") {
("apple-silicon", Provenance::Estimator)
} else {
("estimation", Provenance::Estimator)
};
assert_eq!(composite.active_name(), fallback);
assert_eq!(active.provenance(), Provenance::Estimator);
assert_eq!(reading.provenance, Provenance::Estimator);
assert_eq!(active.provenance(), grade);
assert_eq!(reading.provenance, grade);
let work = Duration::from_millis(150).max(Duration::from_nanos(active.min_window_ns()));
let before = composite.read().expect("the fallback reads");
@ -3638,7 +3651,7 @@ mod tests {
);
assert_eq!(
(before.provenance, after.provenance),
(Provenance::Estimator, Provenance::Estimator),
(grade, grade),
"the fallback's figure must carry the fallback's grade"
);
assert!(

View file

@ -0,0 +1,711 @@
//! Whole-machine power on a Mac, from its System Management Controller:
//! domain [`EnergyDomain::System`], graded [`Provenance::ModelBased`].
//!
//! The SMC publishes the machine's total power as the key `PSTR`, a 4-byte
//! little-endian float in watts. An unprivileged process reads it through the
//! `AppleSMCKeysEndpoint` IOKit user client: no root, no entitlement. Measured
//! on macOS 27.2 on an M5 Max MacBook Pro and an M3 Ultra Mac Studio: both
//! publish it, refreshed about once a second, and the value trails a step in
//! CPU load by about one refresh.
//!
//! **What it covers.** Everything the machine's DC power input feeds: the SoC,
//! memory, storage, fans and the rest of the board, so the work wherever it ran
//! on this machine and everything else the machine powered meanwhile. Not the
//! power supply's own AC-to-DC loss. On the Mac Studio `PSTR` equals the DC
//! input (`PDTR`). On the MacBook Pro, on AC with the battery not charging, it
//! read 51.7 W against 54.9 W at the DC input: it is the system's draw, not the
//! input's.
//!
//! **Grade.** `PSTR` is a power reading, not an energy counter. This counter
//! integrates it over time, a Riemann sum of the SMC's sampled power, so a
//! figure advances whenever time passes and no liveness probe can vouch for it.
//! Like NVML's `power.draw` integral, its readings are graded
//! [`Provenance::ModelBased`] (JouleClaw SPEC §1.3), never `HwShunt`.
//!
//! **Sampling.** One sampler thread per process, started by the first
//! [`SmcSystemCounter::new`], reads `PSTR` every [`POLL`] for the life of the
//! process and integrates the value it holds, so a figure does not depend on how
//! often its caller reads. A read between polls extends the integral to now at
//! the held value, which the next poll integrates over the same time, so
//! readings never run backwards.
//!
//! **Checks.**
//! - `PSTR` must exist and be a 4-byte float (`flt `).
//! - The counter is available only once the sampler has seen the value refresh,
//! within [`FIRST_REFRESH_TIMEOUT`] of its first reading: a value that never
//! moves is not a live reading. The first construction in a process waits for
//! that, about a second.
//! - A running machine never draws 0 W, and no Mac draws more than
//! [`MAX_SYSTEM_WATTS`]. A sample outside that range, or not a number, is
//! discarded: the previous value is held and the sample is counted
//! ([`SmcSystemCounter::implausible_samples`]).
//! - A read gives no figure once the value has held for [`STALE_AFTER`]: the
//! SMC stopped refreshing it, or the sampler stopped reading it.
//!
//! Resolution: 1 mJ, the `ModelBased` floor. Minimum window: [`MIN_WINDOW`],
//! one refresh period of the value plus its lag behind the load.
//!
//! Compiled on macOS with the `smc` feature.
use std::ffi::{CStr, c_char, c_void};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, mpsc};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use crate::meter::EnergyMeter;
use crate::{EnergyCounter, EnergyDomain, EnergyError, EnergyReading, Provenance};
/// How often the sampler reads `PSTR`. A tenth of its ~1 s refresh period, so a
/// new value is integrated from at most 100 ms after the SMC publishes it.
pub const POLL: Duration = Duration::from_millis(100);
/// How long the sampler waits, from its first reading, to see `PSTR` refresh
/// before it declares the counter unavailable: three refresh periods.
pub const FIRST_REFRESH_TIMEOUT: Duration = Duration::from_secs(3);
/// A value that has held this long (ten refresh periods) is not current.
pub const STALE_AFTER: Duration = Duration::from_secs(10);
/// The smallest window a figure means anything over: the value's ~1 s refresh
/// period plus its ~1 s lag behind the load.
pub const MIN_WINDOW: Duration = Duration::from_secs(2);
/// A sample above this is a glitch. It is above the largest Apple Silicon Mac's
/// power supply (the Mac Pro's 1,280 W).
pub const MAX_SYSTEM_WATTS: f32 = 1_500.0;
/// The SMC key for the machine's total power.
const PSTR: u32 = u32::from_be_bytes(*b"PSTR");
/// The SMC data type of a 4-byte float.
const TYPE_FLT: u32 = u32::from_be_bytes(*b"flt ");
// --- IOKit and the SMC user client --------------------------------------------
type IoObject = libc::mach_port_t;
const KERN_SUCCESS: libc::kern_return_t = 0;
/// `kIOMainPortDefault` is `MACH_PORT_NULL`.
const MAIN_PORT_DEFAULT: libc::mach_port_t = 0;
/// The user client's struct method that carries every key request.
const KERNEL_INDEX_SMC: u32 = 2;
/// Request: read a key's bytes.
const SMC_CMD_READ_BYTES: u8 = 5;
/// Request: read a key's size and type.
const SMC_CMD_READ_KEYINFO: u8 = 9;
/// Result: no such key.
const SMC_KEY_NOT_FOUND: u8 = 0x84;
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
static mach_task_self_: libc::mach_port_t;
fn IOServiceMatching(name: *const c_char) -> *mut c_void;
fn IOServiceGetMatchingServices(
main_port: libc::mach_port_t,
matching: *const c_void,
existing: *mut IoObject,
) -> libc::kern_return_t;
fn IOIteratorNext(iterator: IoObject) -> IoObject;
fn IOObjectRelease(object: IoObject) -> libc::kern_return_t;
fn IORegistryEntryGetName(entry: IoObject, name: *mut c_char) -> libc::kern_return_t;
fn IOServiceOpen(
service: IoObject,
owning_task: libc::mach_port_t,
kind: u32,
connect: *mut IoObject,
) -> libc::kern_return_t;
fn IOServiceClose(connect: IoObject) -> libc::kern_return_t;
fn IOConnectCallStructMethod(
connection: IoObject,
selector: u32,
input: *const c_void,
input_size: usize,
output: *mut c_void,
output_size: *mut usize,
) -> libc::kern_return_t;
}
/// The SMC's request and reply, `SMCKeyData_t`: 80 bytes, laid out as the C
/// struct (the asserts below pin each field the SMC reads or writes). The
/// fields this module never reads are still part of the layout the SMC expects.
#[repr(C)]
#[derive(Clone, Copy, Default)]
#[allow(dead_code)]
struct KeyData {
key: u32,
/// `SMCKeyData_vers_t`: major, minor, build, reserved, release (u16).
vers: [u16; 3],
/// `SMCKeyData_pLimitData_t`: version, length (u16 each), then three u32.
p_limit: [u32; 4],
key_info: KeyInfo,
result: u8,
status: u8,
data8: u8,
data32: u32,
bytes: [u8; 32],
}
/// `SMCKeyData_keyInfo_t`. The SMC fills in `data_attributes`; this module
/// does not read it.
#[repr(C)]
#[derive(Clone, Copy, Default)]
#[allow(dead_code)]
struct KeyInfo {
data_size: u32,
data_type: u32,
data_attributes: u8,
}
const _: () = {
assert!(size_of::<KeyData>() == 80);
assert!(std::mem::offset_of!(KeyData, vers) == 4);
assert!(std::mem::offset_of!(KeyData, p_limit) == 12);
assert!(std::mem::offset_of!(KeyData, key_info) == 28);
assert!(std::mem::offset_of!(KeyData, result) == 40);
assert!(std::mem::offset_of!(KeyData, data8) == 42);
assert!(std::mem::offset_of!(KeyData, data32) == 44);
assert!(std::mem::offset_of!(KeyData, bytes) == 48);
};
/// An open connection to the SMC's key endpoint.
struct Smc {
conn: IoObject,
}
impl Smc {
fn open() -> Result<Self, String> {
// SAFETY: a NUL-terminated class name; the returned dictionary is
// consumed by `IOServiceGetMatchingServices`, which is called on it at
// once and not otherwise used.
let matching = unsafe { IOServiceMatching(c"AppleSMC".as_ptr()) };
if matching.is_null() {
return Err("IOServiceMatching(AppleSMC) returned no dictionary".into());
}
let mut iterator: IoObject = 0;
// SAFETY: `matching` is the dictionary just created; `iterator` is a
// valid out pointer.
if unsafe { IOServiceGetMatchingServices(MAIN_PORT_DEFAULT, matching, &mut iterator) } != KERN_SUCCESS {
return Err("no AppleSMC service".into());
}
let mut conn: IoObject = 0;
let mut failure = None;
loop {
// SAFETY: `iterator` is the live iterator returned above.
let service = unsafe { IOIteratorNext(iterator) };
if service == 0 {
break;
}
let mut name = [0 as c_char; 128]; // io_name_t
// SAFETY: `name` is a 128-byte buffer, the size of `io_name_t`.
let named = unsafe { IORegistryEntryGetName(service, name.as_mut_ptr()) } == KERN_SUCCESS
// SAFETY: on success the buffer holds a NUL-terminated name.
&& unsafe { CStr::from_ptr(name.as_ptr()) }.to_bytes() == b"AppleSMCKeysEndpoint";
if named && conn == 0 {
// SAFETY: `service` is a live registry entry; `mach_task_self_`
// is this task's port, set by the runtime before `main`.
let rc = unsafe { IOServiceOpen(service, mach_task_self_, 0, &mut conn) };
if rc != KERN_SUCCESS {
conn = 0;
failure = Some(format!("IOServiceOpen(AppleSMCKeysEndpoint): {rc:#x}"));
}
}
// SAFETY: each entry the iterator returns is ours to release.
unsafe { IOObjectRelease(service) };
}
// SAFETY: the iterator is ours to release.
unsafe { IOObjectRelease(iterator) };
if conn == 0 {
return Err(failure.unwrap_or_else(|| "no AppleSMCKeysEndpoint service".into()));
}
Ok(Self { conn })
}
fn call(&self, input: &KeyData) -> Result<KeyData, String> {
let mut output = KeyData::default();
let mut size = size_of::<KeyData>();
// SAFETY: both buffers are `KeyData`, 80 bytes, and `size` says so.
let rc = unsafe {
IOConnectCallStructMethod(
self.conn,
KERNEL_INDEX_SMC,
(input as *const KeyData).cast(),
size_of::<KeyData>(),
(&mut output as *mut KeyData).cast(),
&mut size,
)
};
if rc != KERN_SUCCESS {
return Err(format!("IOConnectCallStructMethod: {rc:#x}"));
}
match output.result {
0 => Ok(output),
SMC_KEY_NOT_FOUND => Err("no such SMC key".into()),
other => Err(format!("SMC result {other:#x}")),
}
}
}
impl Drop for Smc {
fn drop(&mut self) {
// SAFETY: `conn` is the connection `open` made, closed once.
unsafe { IOServiceClose(self.conn) };
}
}
/// `PSTR`, checked once to be a 4-byte float, read on demand.
struct Pstr {
smc: Smc,
info: KeyInfo,
}
impl Pstr {
fn open() -> Result<Self, String> {
let smc = Smc::open()?;
let info = smc
.call(&KeyData { key: PSTR, data8: SMC_CMD_READ_KEYINFO, ..KeyData::default() })
.map_err(|e| format!("PSTR: {e}"))?
.key_info;
if info.data_size != 4 || info.data_type != TYPE_FLT {
let kind = String::from_utf8_lossy(&info.data_type.to_be_bytes()).into_owned();
return Err(format!("PSTR is {} bytes of type {kind:?}, not a 4-byte float", info.data_size));
}
Ok(Self { smc, info })
}
fn watts(&self) -> Result<f32, String> {
let reply = self
.smc
.call(&KeyData { key: PSTR, key_info: self.info, data8: SMC_CMD_READ_BYTES, ..KeyData::default() })
.map_err(|e| format!("PSTR: {e}"))?;
let [a, b, c, d, ..] = reply.bytes;
Ok(f32::from_le_bytes([a, b, c, d]))
}
}
// --- The integral --------------------------------------------------------------
/// Whether a sample can be a running machine's total power.
fn plausible(watts: f32) -> bool {
watts > 0.0 && watts <= MAX_SYSTEM_WATTS
}
/// `watts` held for `held`, in µJ, rounded down.
fn uj(watts: f32, held: Duration) -> u64 {
(f64::from(watts) * held.as_secs_f64() * 1e6) as u64
}
/// The sampler's integral. Pure: the sampler and the tests drive it with the
/// times and samples they choose.
#[derive(Debug)]
struct State {
/// The value being integrated, W: the last plausible sample.
watts: f32,
/// When the sampler last polled.
polled_at: Instant,
/// When the value last changed.
changed_at: Instant,
/// Energy integrated up to `polled_at`, µJ.
total_uj: u64,
/// Samples discarded as implausible.
implausible: u64,
/// The last poll's failure, cleared by the next reading.
last_error: Option<String>,
}
impl State {
/// The integral from `at`, where the sampler saw `watts` as a fresh value.
fn new(watts: f32, at: Instant) -> Self {
Self { watts, polled_at: at, changed_at: at, total_uj: 0, implausible: 0, last_error: None }
}
/// One poll at `at`: integrate the held value up to `at`, then take the
/// sample as the value to hold from here, if it is plausible.
fn poll(&mut self, at: Instant, sample: Result<f32, String>) {
let held = at.saturating_duration_since(self.polled_at);
self.total_uj = self.total_uj.saturating_add(uj(self.watts, held));
self.polled_at = self.polled_at.max(at);
match sample {
Ok(watts) if plausible(watts) => {
self.last_error = None;
if watts != self.watts {
self.watts = watts;
self.changed_at = self.polled_at;
}
}
Ok(_) => {
self.last_error = None;
self.implausible += 1;
}
Err(e) => self.last_error = Some(e),
}
}
/// The integral at `now`, or why there is no current figure.
fn energy_at(&self, now: Instant) -> Result<u64, String> {
let held = now.saturating_duration_since(self.changed_at);
if held > STALE_AFTER {
let why = match &self.last_error {
Some(e) => format!("; the last read failed: {e}"),
None => String::new(),
};
return Err(format!(
"SMC PSTR has held {} W for {held:?}, more than {STALE_AFTER:?}: not a current reading{why}",
self.watts
));
}
Ok(self.total_uj.saturating_add(uj(self.watts, now.saturating_duration_since(self.polled_at))))
}
}
type Shared = Arc<Mutex<State>>;
fn lock(shared: &Shared) -> MutexGuard<'_, State> {
shared.lock().unwrap_or_else(PoisonError::into_inner)
}
/// Start a sampler over `open`'s reader, polling every `poll`. It reports once
/// it has seen the value refresh, or why it will not run: the reader would not
/// open, its first reading was implausible, or the value did not change within
/// `first_refresh`.
fn start<R>(
open: impl FnOnce() -> Result<R, String> + Send + 'static,
poll: Duration,
first_refresh: Duration,
) -> Result<Shared, String>
where
R: FnMut() -> Result<f32, String> + 'static,
{
let (tx, rx) = mpsc::sync_channel(1);
std::thread::Builder::new()
.name("jouleclaw-smc".into())
.spawn(move || run(tx, open, poll, first_refresh))
.map_err(|e| format!("the SMC sampler thread did not start: {e}"))?;
rx.recv().map_err(|_| "the SMC sampler stopped before it reported".to_string())?
}
fn run<R>(
tx: mpsc::SyncSender<Result<Shared, String>>,
open: impl FnOnce() -> Result<R, String>,
poll: Duration,
first_refresh: Duration,
) where
R: FnMut() -> Result<f32, String>,
{
let mut read = match open() {
Ok(read) => read,
Err(e) => {
let _ = tx.send(Err(e));
return;
}
};
let first = match read() {
Ok(watts) if plausible(watts) => watts,
Ok(watts) => {
let _ = tx.send(Err(format!("the first PSTR reading, {watts} W, is not a running machine's power")));
return;
}
Err(e) => {
let _ = tx.send(Err(e));
return;
}
};
let began = Instant::now();
let fresh = loop {
std::thread::sleep(poll);
match read() {
Ok(watts) if plausible(watts) && watts != first => break watts,
_ if began.elapsed() >= first_refresh => {
let _ = tx.send(Err(format!(
"SMC PSTR held {first} W for {first_refresh:?} after its first reading: not a live reading"
)));
return;
}
_ => {}
}
};
let shared: Shared = Arc::new(Mutex::new(State::new(fresh, Instant::now())));
if tx.send(Ok(Arc::clone(&shared))).is_err() {
return;
}
loop {
std::thread::sleep(poll);
let sample = read();
// The poll's time is taken under the lock, after the SMC read, so no
// reader can have extended the integral past it.
let mut state = lock(&shared);
state.poll(Instant::now(), sample);
}
}
/// The process's one sampler over the real SMC, started on first use.
fn sampler() -> Result<Shared, String> {
static SAMPLER: OnceLock<Result<Shared, String>> = OnceLock::new();
SAMPLER
.get_or_init(|| {
start(
|| {
let pstr = Pstr::open()?;
Ok(move || pstr.watts())
},
POLL,
FIRST_REFRESH_TIMEOUT,
)
})
.clone()
}
// --- The counter -----------------------------------------------------------------
/// The machine's total energy since this process's SMC sampler started, from
/// `PSTR` integrated over time: [`EnergyDomain::System`],
/// [`Provenance::ModelBased`].
pub struct SmcSystemCounter {
sampler: Result<Shared, String>,
}
impl Default for SmcSystemCounter {
fn default() -> Self {
Self::new()
}
}
impl SmcSystemCounter {
/// The counter over this process's SMC sampler. The first call in a process
/// starts the sampler and waits until it has seen `PSTR` refresh (about a
/// second; at most [`FIRST_REFRESH_TIMEOUT`]); every later call shares it.
pub fn new() -> Self {
Self { sampler: sampler() }
}
/// Whether the sampler runs: `PSTR` exists, is a float, read plausibly and
/// was seen to refresh.
pub fn is_available(&self) -> bool {
self.sampler.is_ok()
}
/// Why the counter is unavailable, if it is.
pub fn unavailable_reason(&self) -> Option<&str> {
self.sampler.as_ref().err().map(String::as_str)
}
/// The power being integrated now, W: the last plausible `PSTR` sample.
pub fn watts(&self) -> Option<f32> {
self.sampler.as_ref().ok().map(|s| lock(s).watts)
}
/// How many samples were discarded as implausible (0 W or less, above
/// [`MAX_SYSTEM_WATTS`], or not a number).
pub fn implausible_samples(&self) -> u64 {
self.sampler.as_ref().map_or(0, |s| lock(s).implausible)
}
}
impl EnergyCounter for SmcSystemCounter {
fn domain(&self) -> EnergyDomain {
EnergyDomain::System
}
fn provenance(&self) -> Provenance {
// A sampled power value integrated over time: never a hardware reading.
Provenance::ModelBased
}
fn resolution_uj(&self) -> u64 {
1_000
}
fn min_window_ns(&self) -> u64 {
MIN_WINDOW.as_nanos() as u64
}
fn read(&self) -> Result<EnergyReading, EnergyError> {
let shared = self.sampler.as_ref().map_err(|_| EnergyError::NoCounter(EnergyDomain::System))?;
let uj = lock(shared).energy_at(Instant::now()).map_err(EnergyError::Platform)?;
let timestamp_ns = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| EnergyError::Platform(e.to_string()))?
.as_nanos() as u64;
Ok(EnergyReading { uj, timestamp_ns, domain: EnergyDomain::System, provenance: Provenance::ModelBased })
}
}
impl EnergyMeter for SmcSystemCounter {
fn name(&self) -> &'static str {
"apple-smc"
}
fn is_available(&self) -> bool {
self.sampler.is_ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
const MS: Duration = Duration::from_millis(1);
fn counter(sampler: Result<Shared, String>) -> SmcSystemCounter {
SmcSystemCounter { sampler }
}
#[test]
fn the_layout_is_the_smc_s() {
// Pinned at compile time; restated so a reader sees the size here.
assert_eq!(size_of::<KeyData>(), 80);
assert_eq!(PSTR.to_be_bytes(), *b"PSTR");
assert_eq!(TYPE_FLT.to_be_bytes(), *b"flt ");
}
/// Each poll integrates the value held since the previous one, and a read
/// between polls extends it to now at the held value.
#[test]
fn the_held_value_is_integrated() {
let t0 = Instant::now();
let mut s = State::new(10.0, t0);
s.poll(t0 + 100 * MS, Ok(10.0));
assert_eq!(s.total_uj, 1_000_000, "10 W for 100 ms");
s.poll(t0 + 200 * MS, Ok(20.0));
assert_eq!(s.total_uj, 2_000_000, "the second 100 ms is still at the held 10 W");
assert_eq!(s.energy_at(t0 + 250 * MS), Ok(3_000_000), "then 20 W for 50 ms");
}
/// A read between polls never exceeds what the next poll integrates, so a
/// sequence of reads never runs backwards.
#[test]
fn reads_never_run_backwards() {
let t0 = Instant::now();
let mut s = State::new(50.0, t0);
let mut last = 0;
for i in 1..=200u32 {
let at = t0 + i * 37 * MS / 10;
if i % 3 == 0 {
s.poll(at, Ok(40.0 + (i % 17) as f32));
}
let e = s.energy_at(at).expect("current");
assert!(e >= last, "read {i}: {e} < {last}");
last = e;
}
}
/// A sample that cannot be a running machine's power is not integrated:
/// the previous value is held, and the sample is counted.
#[test]
fn implausible_samples_are_held_over_and_counted() {
let t0 = Instant::now();
let mut s = State::new(30.0, t0);
let bad = [0.0, -4.0, f32::NAN, f32::INFINITY, MAX_SYSTEM_WATTS + 1.0];
for (i, w) in bad.into_iter().enumerate() {
s.poll(t0 + (i as u32 + 1) * 100 * MS, Ok(w));
}
assert_eq!(s.watts, 30.0);
assert_eq!(s.implausible, bad.len() as u64);
assert_eq!(s.total_uj, 15_000_000, "30 W held for 500 ms");
s.poll(t0 + 600 * MS, Ok(MAX_SYSTEM_WATTS));
assert_eq!(s.watts, MAX_SYSTEM_WATTS, "the ceiling itself is plausible");
}
/// A value that has held for longer than `STALE_AFTER` gives no figure,
/// whether the SMC kept returning it or the reads failed; a fresh value
/// brings the figure back.
#[test]
fn a_held_value_goes_stale() {
let t0 = Instant::now();
let mut s = State::new(12.5, t0);
for i in 1..=110u32 {
s.poll(t0 + i * 100 * MS, Ok(12.5));
}
let held = s.energy_at(t0 + 11_000 * MS).expect_err("held 11 s");
assert!(held.contains("12.5 W") && held.contains("not a current reading"), "{held}");
s.poll(t0 + 11_100 * MS, Ok(13.0));
assert!(s.energy_at(t0 + 11_100 * MS).is_ok());
let mut s = State::new(12.5, t0);
for i in 1..=110u32 {
s.poll(t0 + i * 100 * MS, Err("IOConnectCallStructMethod: 0xe00002c2".into()));
}
assert_eq!(s.total_uj, uj(12.5, 11_000 * MS), "failed reads hold the value until it is stale");
let failed = s.energy_at(t0 + 11_000 * MS).expect_err("stale");
assert!(failed.contains("0xe00002c2"), "{failed}");
}
/// The sampler reports only once it has seen the value refresh, and says
/// why it will not run otherwise.
#[test]
fn the_sampler_starts_only_on_a_refresh() {
let script = |values: Vec<Result<f32, String>>| {
move || {
let mut values = values.into_iter();
Ok(move || values.next().unwrap_or(Ok(7.0)))
}
};
let ok = start(script(vec![Ok(5.0), Ok(5.0), Ok(6.0)]), MS, 50 * MS).expect("a refresh was seen");
assert_eq!(lock(&ok).watts, 6.0, "the integral starts at the refresh");
let held = start(|| Ok(|| Ok(5.0)), MS, 30 * MS).expect_err("never refreshed");
assert!(held.contains("held 5 W") && held.contains("not a live reading"), "{held}");
let first = start(script(vec![Ok(0.0)]), MS, 30 * MS).expect_err("0 W first");
assert!(first.contains("not a running machine's power"), "{first}");
let closed = start(|| Err::<fn() -> Result<f32, String>, _>("no AppleSMCKeysEndpoint service".into()), MS, 30 * MS)
.expect_err("did not open");
assert_eq!(closed, "no AppleSMCKeysEndpoint service");
let err = start(script(vec![Err("PSTR: no such SMC key".into())]), MS, 30 * MS).expect_err("no key");
assert_eq!(err, "PSTR: no such SMC key");
}
/// The counter's contract: whole-machine domain, a `ModelBased` grade on
/// every reading, and no reading without a sampler.
#[test]
fn the_counter_declares_its_domain_and_grade() {
let none = counter(Err("no AppleSMCKeysEndpoint service".into()));
assert!(!none.is_available());
assert_eq!(none.unavailable_reason(), Some("no AppleSMCKeysEndpoint service"));
assert!(matches!(none.read(), Err(EnergyError::NoCounter(EnergyDomain::System))));
assert_eq!((none.watts(), none.implausible_samples()), (None, 0));
let shared = Arc::new(Mutex::new(State::new(20.0, Instant::now())));
let c = counter(Ok(shared));
assert_eq!((c.domain(), c.provenance(), c.name()), (EnergyDomain::System, Provenance::ModelBased, "apple-smc"));
assert!(c.resolution_uj() >= 1_000);
assert_eq!(c.min_window_ns(), 2_000_000_000);
let r = c.read().expect("reads");
assert_eq!((r.domain, r.provenance), (EnergyDomain::System, Provenance::ModelBased));
assert_eq!(c.watts(), Some(20.0));
}
/// On this Mac: the SMC's `PSTR`, read with no privilege, meters a CPU
/// burn longer than the minimum window as a figure a machine can draw.
#[test]
fn the_smc_meters_this_mac() {
let c = SmcSystemCounter::new();
let Some(watts) = c.watts() else {
eprintln!("apple-smc: unavailable: {}", c.unavailable_reason().unwrap_or("?"));
return;
};
let window = MIN_WINDOW + Duration::from_millis(500);
let r0 = c.read().expect("an available counter reads");
let t = Instant::now();
let mut x: u64 = 0x9E37_79B9_7F4A_7C15;
while t.elapsed() < window {
for _ in 0..100_000 {
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
}
}
std::hint::black_box(x);
let r1 = c.read().expect("an available counter reads");
let took = t.elapsed();
let uj = r1.uj.checked_sub(r0.uj).expect("never runs backwards");
let average = uj as f64 / took.as_secs_f64() / 1e6;
eprintln!(
"apple-smc: measured {uj} µJ over {took:?} = {average:.1} W [{}] (PSTR {watts:.1} W at start, {} implausible)",
r1.provenance,
c.implausible_samples()
);
assert_eq!((r1.domain, r1.provenance), (EnergyDomain::System, Provenance::ModelBased));
assert!(average > 0.5 && average <= f64::from(MAX_SYSTEM_WATTS), "{average} W is not a running Mac's draw");
}
}

View file

@ -12,7 +12,10 @@
//! counter's resolution.
//! - [`Provenance::ModelBased`] — a vendor-provided model estimate derived
//! from frequency, voltage, and utilization (NVML `power.draw` on older
//! GPUs, ROCm-SMI `power1_average`). Millijoule-quantized at best.
//! GPUs, ROCm-SMI `power1_average`), or a vendor's sampled power integrated
//! over time (the Apple SMC's whole-machine `PSTR`, domain
//! [`EnergyDomain::System`], with the `smc` feature). Millijoule-quantized at
//! best.
//! - [`Provenance::Estimator`] — a JouleClaw static cost model derived from
//! architecture × precision × batch tables. Used only on platforms
//! with no usable hardware counter (consumer AMD GPUs, ARM PMU).
@ -86,6 +89,10 @@ pub mod apple_ioreport;
#[cfg(all(feature = "ioreport", target_os = "macos"))]
mod apple_gpu;
// The whole machine's power on a Mac (SMC `PSTR`), integrated over time.
#[cfg(all(feature = "smc", target_os = "macos"))]
pub mod apple_smc;
// This process's own GPU energy (Apple Silicon), and a span of in-process work
// on its CPU and GPU together. Always compiled; the GPU counter is available
// only on macOS with the `ioreport` feature. (Their docs are their own `//!`.)
@ -117,9 +124,10 @@ pub enum Provenance {
/// channels whose CPU rails are all live, Jetson INA3221 i2c.
HwShunt,
/// Vendor-provided model estimate derived from frequency, voltage,
/// and utilization. Millijoule-quantized at best; not a true
/// measurement. Examples: NVML `power.draw` interpolation, ROCm-SMI
/// `power1_average`.
/// and utilization, or a vendor's sampled power integrated over time.
/// Millijoule-quantized at best; not a true measurement. Examples: NVML
/// `power.draw` interpolation, ROCm-SMI `power1_average`, the Apple SMC's
/// system power `PSTR`.
ModelBased,
/// JouleClaw static cost model from architecture × precision × batch
/// tables. Used only when no hardware counter exists. Tolerance
@ -292,6 +300,13 @@ pub enum EnergyDomain {
/// client's GPU time over the span. Not the CPU that encoded the work, not
/// the ANE, not DRAM.
ProcessGpu,
/// The **whole machine**: everything its DC power input feeds — the SoC,
/// memory, storage, fans and the rest of the board — as one figure (on a
/// Mac, the SMC's system-total power `PSTR`, `apple_smc::SmcSystemCounter`).
/// It holds [`Self::SocTotal`] and every other domain on the machine, so it
/// covers work wherever it ran here, and also everything else the machine
/// powered meanwhile. Not the power supply's own AC-to-DC loss.
System,
}
/// How much of a unit of work a figure accounts for (the family's coverage

View file

@ -257,7 +257,10 @@ impl EnergyMeter for EstimationMeter {
///
/// With the `ioreport` feature on macOS, the IOReport counter is selected only
/// when [`crate::ioreport_status`] is `Live`; the first call in a process runs
/// that liveness probe on the calling thread. RAPL (`rapl`, Linux) is selected
/// that liveness probe on the calling thread. With the `smc` feature on macOS,
/// the SMC's whole-machine power integral (`ModelBased`, over
/// [`EnergyDomain::System`]) comes next; the first call starts its sampler and
/// waits to see `PSTR` refresh, about a second. RAPL (`rapl`, Linux) is selected
/// only when every package counter is live ([`crate::rapl::rapl_liveness`]); NVML (`nvml`)
/// reads its cumulative-energy counter as a hardware reading only when that is
/// live, and otherwise its power-draw integral, graded `ModelBased`. Each of
@ -283,9 +286,26 @@ pub fn detect_meter() -> CompositeMeter {
// an M3 Ultra), and the
// summed rails then move only with the GPU, or not at all.
// `crate::ioreport_status()` says which case this host is in. When it
// is unavailable, `CompositeMeter` falls through to the load-average
// estimator below, whose readings say `Provenance::Estimator`.
// is unavailable, `CompositeMeter` falls through: to the SMC's
// whole-machine integral where `smc` is on and `PSTR` is live
// (`Provenance::ModelBased`), else to the load-average estimator below,
// whose readings say `Provenance::Estimator`.
meters.push(Box::new(crate::apple_ioreport::AppleIoReportCounter::new()));
}
#[cfg(all(feature = "smc", target_os = "macos"))]
{
// The whole machine's power from the SMC (`PSTR`), integrated over
// time: `ModelBased`, over `EnergyDomain::System`. After a live IOReport
// counter, which is a hardware reading over a narrower domain; before
// the load-average estimator, which reads no power at all. Its domain
// holds the SoC and everything else the machine powers, so a figure
// from it may be high for the work, never low.
meters.push(Box::new(crate::apple_smc::SmcSystemCounter::new()));
}
#[cfg(all(feature = "ioreport", target_os = "macos"))]
{
meters.push(Box::new(crate::apple::AppleSiliconMeter::new()));
}
@ -390,6 +410,36 @@ mod tests {
assert!(!names.contains(&"process-cpu"), "{names:?}");
}
/// With the `smc` feature on macOS the SMC's whole-machine integral sits
/// after the IOReport counter and before the load-average estimator, and it
/// is the active meter wherever IOReport is not live and `PSTR` is.
#[test]
#[cfg(all(feature = "smc", target_os = "macos"))]
fn the_smc_meter_sits_between_ioreport_and_the_estimator() {
let m = detect_meter();
let apple: Vec<&str> = m.names().into_iter().filter(|n| n.starts_with("apple-")).collect();
let want: &[&str] =
if cfg!(feature = "ioreport") { &["apple-ioreport", "apple-smc", "apple-silicon"] } else { &["apple-smc"] };
assert_eq!(apple, want);
let ioreport_live = crate::ioreport_status() == crate::IoReportStatus::Live;
let smc = crate::apple_smc::SmcSystemCounter::new();
let expected = if ioreport_live {
"apple-ioreport"
} else if smc.is_available() {
"apple-smc"
} else if cfg!(all(feature = "ioreport", target_arch = "aarch64")) {
"apple-silicon"
} else {
"estimation"
};
eprintln!("apple-smc: detect_meter holds {:?}, active {}", m.names(), m.active_name());
assert_eq!(m.active_name(), expected, "{:?}", smc.unavailable_reason());
if expected == "apple-smc" {
let r = m.read().expect("the SMC meter reads");
assert_eq!((r.domain, r.provenance), (EnergyDomain::System, Provenance::ModelBased));
}
}
/// `detect_in_process_meter` prefers the process counter where it is live,
/// and is `detect_meter` where it is not (and off macOS).
#[test]

View file

@ -358,10 +358,14 @@ mod hw {
/// request ran on that process's threads alone, and it misses any GPU work
/// and any other process serving it. Nor does one process's own GPU energy
/// (`ProcessGpu`), for the same reasons and because its CPU is not in it.
/// The match is exhaustive, so a new domain has to be decided here.
/// The whole machine (`System`, a Mac's SMC system power) does: it holds
/// every part the request ran on, and everything else the machine powered
/// meanwhile, so its figure may be high for the request but never misses
/// part of it. The match is exhaustive, so a new domain has to be decided
/// here.
pub fn covers_whole_request(domain: EnergyDomain) -> bool {
match domain {
EnergyDomain::SocTotal | EnergyDomain::CpuPkg => true,
EnergyDomain::SocTotal | EnergyDomain::CpuPkg | EnergyDomain::System => true,
EnergyDomain::CpuCore
| EnergyDomain::Dram
| EnergyDomain::Gpu
@ -681,11 +685,12 @@ mod hw {
/// domain added to the family meter fails to compile in
/// `covers_whole_request` before it can be graded here by accident.
#[test]
fn only_the_soc_or_a_cpu_package_covers_the_whole_request() {
fn only_the_soc_a_cpu_package_or_the_machine_covers_the_whole_request() {
use EnergyDomain::*;
for (d, whole) in [
(SocTotal, true),
(CpuPkg, true),
(System, true),
(CpuCore, false),
(Dram, false),
(Gpu, false),