Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Has been cancelled
standards workspaces build + test / jouleclaw-rs (pull_request) Has been cancelled
standards workspaces build + test / aac-rs (pull_request) Has been cancelled
standards workspaces build + test / arl-rs (pull_request) Has been cancelled
standards workspaces build + test / eoc-rs (pull_request) Has been cancelled
standards workspaces build + test / jcp-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (pull_request) Has been cancelled
standards workspaces build + test / joulecontract-rs (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (pull_request) Has been cancelled
standards workspaces build + test / map-rs (pull_request) Has been cancelled
standards workspaces build + test / mesh-rs (pull_request) Has been cancelled
standards workspaces build + test / openpay (pull_request) Has been cancelled
standards workspaces build + test / proof-rs (pull_request) Has been cancelled
standards workspaces build + test / sandbox-rs (pull_request) Has been cancelled
standards workspaces build + test / sense-rs (pull_request) Has been cancelled
standards workspaces build + test / smart-byte-rs (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Has been cancelled
joule-ui's energy record carries one figure per interaction and has no coverage field. Both `SpanMeter` adapters (joule-ui-serve's, and the examples' `support/span_meter.rs`) graded a span by its records alone, so a span from an NVML-only probe set (a Linux host whose only live counter is the GPU's: a `Gpu` record, `HwShunt`, and no CPU record at all) was sealed `HwShunt` for the whole interaction: the GPU's energy, passed off as a reading of a handler that ran on the CPU. `Span::stop`'s held-CPU rule does not reach it, because there is no CPU counter to have held. The rule now: a span with no hardware reading of the CPU (`Measurement::reads_cpu`) is sealed as the meter's `Estimator` figure for that span's wall time (`PowerMeter::estimate`, wall x the meter's TDP, never 0 for work that ran). A span that reads the CPU passes through unchanged, with any GPU record beside it (RAPL and NVML: the sum, `HwShunt`). A span that was already the estimator's record is replaced by the same figure. The rule is one function, `interaction_figure`, applied where each adapter closes a span. The examples' adapter is now generic over the same `Spans` seam joule-ui-serve's has (defaulting to `PowerMeter`), so it can be driven over scripted spans, and a new integration test includes it by `#[path]`. SPEC §5 gains the sentence the shared vendor-counter MUST implies for this format: with no coverage field, a figure from a counter that does not read the domain the work ran on (a GPU counter alone, for a handler on the CPU) MUST NOT be graded `HwShunt` for the interaction. joule-ui-serve's Cargo comment no longer lists NVML alone as a source of a `HwShunt` receipt. Tests: `a_gpu_only_span_is_sealed_as_an_estimate_never_as_a_reading`, in joule-ui-serve (through the real `Host`) and in joule-ui-interaction's new `tests/span_meter.rs` (through the real `run_interaction` and the examples' adapter): a scripted 8 ms GPU-only span (9,000 uJ `HwShunt`) is sealed as 160,000 uJ `Estimator` with no measured figure; a CPU+GPU span is sealed `HwShunt` at their sum (10,500 uJ); a CPU-only span as itself; the chain verifies. joule-ui-serve, -interaction, -conformance, -mcp, -core, -a2ui and -render pass (72 tests); clippy adds nothing (one existing joule-ui-core lint); `real_meter` and `jcp_metered` run as before on this M5 Max (every interaction `Estimator`, the 15 J grant spent and the sixth denied). `gen_vectors` regenerated into a scratch directory is byte-identical to `conformance/v1/` (`diff -r` clean, MANIFEST included): the conformance pack's `StepMeter` is not a `SpanMeter`, and nothing in the signed core changed. Cargo.lock gains only the new sandbox-schema dev-dependency edge. Falsified, each restored from git and re-run green: either adapter sealing the span as it stands (`interaction_figure` returning it unchanged) fails its `a_gpu_only_span_is_sealed_as_an_estimate_never_as_a_reading`. Co-Authored-By: Claude Opus 5.5 <[email protected]>
86 lines
4 KiB
Rust
86 lines
4 KiB
Rust
//! The examples' `SpanMeter` (`examples/support/span_meter.rs`), driven over
|
|
//! scripted spans through the real `run_interaction`: each interaction is
|
|
//! sealed with its own span's figure and grade, and a span with no reading of
|
|
//! the CPU is never sealed as a hardware reading of the interaction.
|
|
|
|
#[path = "../examples/support/span_meter.rs"]
|
|
#[allow(dead_code)]
|
|
mod span_meter;
|
|
|
|
use joule_ui_interaction::{ContentId, InteractionLog, Provenance, run_interaction, verify_chain};
|
|
use sandbox_meter::{DeviceEnergy, Measurement, Provenance as Grade};
|
|
use sandbox_schema::DeviceClass;
|
|
use span_meter::{SpanMeter, Spans};
|
|
use std::collections::VecDeque;
|
|
use std::time::Duration;
|
|
|
|
/// A meter whose spans replay a script, one per close, and whose estimate is
|
|
/// 20 W, the Sandbox estimator's default.
|
|
struct Script(VecDeque<Measurement>);
|
|
|
|
impl Spans for Script {
|
|
type Open = ();
|
|
fn open(&mut self) {}
|
|
fn close(&mut self, _span: ()) -> Measurement {
|
|
self.0.pop_front().expect("script exhausted")
|
|
}
|
|
fn estimate(&self, wall: Duration) -> Measurement {
|
|
let uj = (wall.as_secs_f64() * 20e6) as u64;
|
|
Measurement { devices: vec![record(DeviceClass::Cpu, Grade::Estimator, uj)], wall, source: "estimator" }
|
|
}
|
|
}
|
|
|
|
fn record(class: DeviceClass, provenance: Grade, uj: u64) -> DeviceEnergy {
|
|
DeviceEnergy { class, measured_uj: (provenance == Grade::HwShunt).then_some(uj), estimated_uj: uj, provenance }
|
|
}
|
|
|
|
/// An 8 ms span of these records.
|
|
fn span_of(devices: Vec<DeviceEnergy>, source: &'static str) -> Measurement {
|
|
Measurement { devices, wall: Duration::from_millis(8), source }
|
|
}
|
|
|
|
/// Seal `n` interactions over the scripted spans — the first interaction's
|
|
/// own span, then for each later one the idle gap before it and its own span
|
|
/// — through the real `run_interaction`, returning the log.
|
|
fn seal(script: Vec<Measurement>, n: u8) -> InteractionLog {
|
|
assert_eq!(script.len(), 2 * usize::from(n) - 1, "setup: one span for the first, two for each later one");
|
|
let meter = SpanMeter::new(Script(script.into()));
|
|
let surface = ContentId::of_bytes(b"<Form><Slider action='apply'/></Form>");
|
|
let mut log = InteractionLog::new(ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]));
|
|
for i in 1..=n {
|
|
log.record(run_interaction(&surface, "apply", "did:key:z6MkTestActor", &[i], &meter, None, || vec![i]));
|
|
}
|
|
log
|
|
}
|
|
|
|
/// A GPU-only span — NVML's reading, no CPU record, as on a Linux host whose
|
|
/// only live counter is the GPU's — is not a reading of the interaction,
|
|
/// which ran on the CPU. It is sealed as the meter's estimate for the span's
|
|
/// 8 ms (160 000 µJ at 20 W), `Estimator`: not the GPU's 9 000 µJ as
|
|
/// `HwShunt`, and not 0. The span that reads the CPU beside the GPU (RAPL and
|
|
/// NVML) is a reading, sealed as one; and one that reads only the CPU is too.
|
|
#[test]
|
|
fn a_gpu_only_span_is_sealed_as_an_estimate_never_as_a_reading() {
|
|
let gpu_only = span_of(vec![record(DeviceClass::Gpu, Grade::HwShunt, 9_000)], "nvml-total-energy");
|
|
assert!(gpu_only.fully_measured() && !gpu_only.reads_cpu(), "setup: a GPU reading with no CPU record");
|
|
let gap = || span_of(vec![record(DeviceClass::Cpu, Grade::Estimator, 999_999)], "estimator");
|
|
let both = span_of(
|
|
vec![record(DeviceClass::Cpu, Grade::HwShunt, 1_500), record(DeviceClass::Gpu, Grade::HwShunt, 9_000)],
|
|
"composite",
|
|
);
|
|
let cpu = span_of(vec![record(DeviceClass::Cpu, Grade::HwShunt, 2_000)], "rapl-powercap");
|
|
let log = seal(vec![gpu_only, gap(), both, gap(), cpu], 3);
|
|
|
|
assert_eq!(verify_chain(log.entries()), Ok(3));
|
|
let sealed: Vec<_> = log.entries().iter().map(|e| e.interaction.energy).collect();
|
|
let got: Vec<_> = sealed.iter().map(|e| (e.provenance, e.measured_uj, e.estimated_uj)).collect();
|
|
assert_eq!(
|
|
got,
|
|
[
|
|
(Provenance::Estimator, None, 160_000),
|
|
(Provenance::HwShunt, Some(10_500), 10_500),
|
|
(Provenance::HwShunt, Some(2_000), 2_000),
|
|
],
|
|
"{sealed:?}"
|
|
);
|
|
}
|