open-standards/joule-ui/joule-ui-rs/crates/joule-ui-interaction/examples/support/span_meter.rs
Transaction Science 7539c62e19
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: a span with no CPU reading is sealed as an estimate of the interaction, never as HwShunt
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]>
2026-09-30 20:47:34 -04:00

115 lines
4.6 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The Sandbox `PowerMeter` as a joule-ui `Meter` (SPEC §5 edge) over managed
//! spans, shared by the `real_meter` and `jcp_metered` examples. It is the
//! adapter `joule-ui-serve` seals its served interactions with.
//!
//! Every `sample_uj` closes the span the previous sample opened, adds that
//! span's figure to a running counter, and opens the next. So the difference
//! of two consecutive samples is exactly one span's figure, and `provenance`
//! is that span's own, which `run_interaction` asks for right after its second
//! sample. A span is `HwShunt` only when it was a hardware reading of the CPU
//! the handler ran on, whose CPU counter advanced. Otherwise it is the meter's
//! `Estimator` figure (wall-clock × TDP, never 0 for work that ran): on a host
//! with no live energy counter — every macOS 27.0.1 host checked, whose CPU
//! energy rail is frozen — when the span's CPU counter held while another
//! class moved, and when the span has no CPU reading at all (an NVML-only
//! host reads the GPU alone: see [`interaction_figure`]).
//!
//! The examples' old adapter sampled `PowerMeter::sample_uj`, a sum of raw
//! counters under the trait's default `HwShunt`. Where no hardware probe
//! existed that was `None`, and each interaction that ran was sealed at
//! `0 µJ [Estimator]`.
//!
//! `tests/span_meter.rs` includes this file too, and drives the adapter over
//! scripted spans.
use joule_ui_interaction::{Meter, Provenance};
use sandbox_meter::probe::Span;
use sandbox_meter::{Measurement, PowerMeter};
use std::cell::RefCell;
use std::time::Duration;
/// A meter that measures managed spans: the Sandbox `PowerMeter` does, and a
/// test can script one.
pub trait Spans {
type Open;
fn open(&mut self) -> Self::Open;
fn close(&mut self, span: Self::Open) -> Measurement;
/// The meter's estimate for `wall` of work, with no reading.
fn estimate(&self, wall: Duration) -> Measurement;
}
impl Spans for PowerMeter {
type Open = Span;
fn open(&mut self) -> Span {
self.start()
}
fn close(&mut self, span: Span) -> Measurement {
span.stop(self)
}
fn estimate(&self, wall: Duration) -> Measurement {
PowerMeter::estimate(self, wall)
}
}
/// `RefCell` because sampling mutates the meter while the `Meter` trait
/// samples through `&self`. Each example drives one interaction at a time, so
/// a span is exclusive to one interaction.
pub struct SpanMeter<M: Spans = PowerMeter>(RefCell<State<M>>);
struct State<M: Spans> {
meter: M,
open: Option<M::Open>,
counter_uj: u64,
last: Provenance,
}
impl<M: Spans> SpanMeter<M> {
pub fn new(meter: M) -> Self {
SpanMeter(RefCell::new(State { meter, open: None, counter_uj: 0, last: Provenance::Estimator }))
}
}
impl<M: Spans> Meter for SpanMeter<M> {
fn sample_uj(&self) -> Option<u64> {
let mut s = self.0.borrow_mut();
if let Some(span) = s.open.take() {
let m = s.meter.close(span);
let m = interaction_figure(m, |wall| s.meter.estimate(wall));
s.counter_uj = s.counter_uj.saturating_add(m.total_uj());
s.last = span_provenance(&m);
}
let next = s.meter.open();
s.open = Some(next);
Some(s.counter_uj)
}
/// The provenance of the span the last sample closed.
fn provenance(&self) -> Provenance {
self.0.borrow().last
}
}
/// The figure an interaction is sealed with, from its span. joule-ui seals
/// one figure per interaction, with no field to say that figure covers part
/// of the work, so a span with no hardware reading of the CPU is not a
/// reading of the interaction: an NVML-only probe set reads the GPU alone,
/// and its figure would be sealed `HwShunt` for work that ran on the CPU. Such
/// a span is sealed as the meter's estimate for its wall time (`Estimator`,
/// never 0 for work that ran); a span that reads the CPU passes through,
/// with any GPU record beside it.
fn interaction_figure(m: Measurement, estimate: impl FnOnce(Duration) -> Measurement) -> Measurement {
if m.reads_cpu() { m } else { estimate(m.wall) }
}
/// A span's provenance: the weakest of its records, because a sum is only as
/// good as its weakest part.
fn span_provenance(m: &Measurement) -> Provenance {
use sandbox_meter::Provenance as Grade;
if m.devices.is_empty() || m.devices.iter().any(|d| d.provenance == Grade::Estimator) {
Provenance::Estimator
} else if m.devices.iter().all(|d| d.provenance == Grade::HwShunt && d.measured_uj.is_some()) {
Provenance::HwShunt
} else {
Provenance::ModelBased
}
}