All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1m39s
standards workspaces build + test / aac-rs (pull_request) Successful in 36s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 37s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 32s
standards workspaces build + test / map-rs (pull_request) Successful in 30s
standards workspaces build + test / mesh-rs (pull_request) Successful in 24s
standards workspaces build + test / openpay (pull_request) Successful in 1m23s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m27s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m48s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m20s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m9s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m32s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m12s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 4m37s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m3s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 3m0s
standards workspaces build + test / sense-rs (pull_request) Successful in 3m11s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 4m30s
joule-ui-serve's action handlers and the joule-ui-interaction examples' handlers
are in-process work, verified rather than assumed: `Host::handle` calls the
handler inside `run_interaction`, on the worker thread that holds the host
lock, and nothing in joule-ui-serve or the examples spawns a process. So they
now meter with `PowerMeter::detect_in_process()`, and on a host whose
per-process CPU energy counter is live (both Apple Silicon hosts checked,
macOS 27.0.1, where the machine-wide CPU rail is frozen) each interaction is
sealed as a `HwShunt` reading of the energy the kernel attributes to this
process's threads over the interaction, instead of the 20 W wall-clock
estimate. Where the counter is not live the meter is `detect()`'s, as before.
The figure is this process's, not one thread's: in joule-ui-serve it includes
any worker that served a socket during the span.
joule-ui-serve: `main` serves with `served_power_meter()`, which is
`detect_in_process()`, and the end-to-end test now builds its meter from that
same function and asserts, where the counter is live, that the meter is the
process probe and that each ~24 ms served interaction is `HwShunt`.
The examples: `real_meter` and `jcp_metered` use `detect_in_process()` and
say which counter they read. With real readings an interaction of ~150 ms
costs a fraction of a joule, not the estimate's 3 J, so six fires no longer
spend `jcp_metered`'s 15 J grant; it now fires until the real JCP runtime
denies one (at most 200), printing the first five and the last three, so the
budget binds whatever the figures are. The grant itself is unchanged.
SPEC §5 names the per-process counter among the reference meter's sources;
the two Cargo comments say which meter each crate uses.
Tests: joule-ui-serve, -interaction, -conformance, -mcp, -core, -a2ui and
-render pass (72). The served interactions on this M5 Max: `Energy {
measured_uj: Some(25908), ... provenance: HwShunt }` and `Some(23193)`.
`gen_vectors` regenerated into a scratch directory is byte-identical to
`conformance/v1/` (`diff -r` clean). The examples, release build, here:
real_meter: render #1: allow · 195798 µJ [HwShunt] ... sealed chain: 3 links
verified, 584156 µJ total — hardware counter deltas (HwShunt), this
process's CPU energy over each interaction
jcp_metered: 82 interactions sealed: 81 allowed; 15001229 µJ sealed,
hardware counter deltas (HwShunt) from process-energy; 15000000 of the
15000000 µJ budget debited; 1 denied by the real JCP runtime once the budget
was spent.
(The per-interaction figure varied from 0.04 to 0.21 J: the handlers burn for
150 ms of wall time, and other builds were loading this machine.)
Falsified, restored from git and re-run green: `served_power_meter` returning
`detect()` fails `the_served_meter_never_seals_zero_or_an_estimate_as_a_reading`.
Rebased onto 7cf1f036 with the commit before it; this commit's files are
unchanged. joule-ui-interaction, -serve, -conformance, -mcp, -core, -a2ui
and -render pass, and `gen_vectors` regenerated into a scratch directory is
still byte-identical to `conformance/v1/`.
Rebased again, onto a16e36cf, with the commit before it; this commit's
files are unchanged. The seven joule-ui crates pass (72), and `gen_vectors`
regenerated into a scratch directory is still byte-identical to
`conformance/v1/`.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
108 lines
5.1 KiB
Rust
108 lines
5.1 KiB
Rust
//! A joule-ui interaction metered by the **real Sandbox `PowerMeter`** — the
|
||
//! reference Meter edge (SPEC §5) — rather than a stub. Proves the
|
||
//! recognized-edge design composes end to end: a fired Action's energy
|
||
//! record is sealed into the chain.
|
||
//!
|
||
//! What that record is depends on the host, and the output says which. Each
|
||
//! interaction is metered as one `PowerMeter` span and sealed with that
|
||
//! span's own figure and provenance (the `SpanMeter` in `support/`, the
|
||
//! adapter `joule-ui-serve` uses). The handlers run on this process's own
|
||
//! thread and spawn nothing, so the meter is `PowerMeter::detect_in_process()`.
|
||
//! Where this process's own CPU energy counter is live (every Apple Silicon
|
||
//! host checked, macOS 27.0.1) and advanced over the span, the figure is
|
||
//! `HwShunt`: the energy the kernel attributes to this process's threads over
|
||
//! the interaction. Where it is not, the meter is `detect()`'s: a live
|
||
//! machine-wide counter (Intel/AMD RAPL; Apple Silicon IOReport only when its
|
||
//! CPU rail is live) gives `HwShunt`, the machine-wide delta; with none, it is
|
||
//! the meter's `Estimator` figure, wall-clock × TDP for the interaction's own
|
||
//! span: an estimate, labelled as one, and never 0 µJ for work that ran.
|
||
//!
|
||
//! ```bash
|
||
//! cargo run -p joule-ui-interaction --example real_meter --release
|
||
//! ```
|
||
|
||
use joule_ui_interaction::{ContentId, Energy, InteractionLog, Provenance, run_interaction, verify_chain};
|
||
use sandbox_meter::PowerMeter;
|
||
use std::time::{Duration, Instant};
|
||
|
||
#[path = "support/span_meter.rs"]
|
||
mod span_meter;
|
||
use span_meter::SpanMeter;
|
||
|
||
/// What an interaction's figure is, read from its own record: a 0 for work
|
||
/// that ran is no figure, and only a hardware record is called a reading.
|
||
fn figure(e: &Energy) -> String {
|
||
match (e.uj(), e.provenance) {
|
||
(0, p) => format!("no figure (0 µJ [{}]: the meter gave no reading for this work)", p.label()),
|
||
(uj, Provenance::HwShunt) => format!("{uj} µJ [HwShunt]"),
|
||
(uj, p) => format!("{uj} µJ [{}, an estimate]", p.label()),
|
||
}
|
||
}
|
||
|
||
fn main() {
|
||
// The handlers below run on this thread and spawn nothing: in-process work.
|
||
let pm = PowerMeter::detect_in_process();
|
||
let hardware = pm.is_hardware();
|
||
let hardware_in_process = pm.sources().contains(&"process-energy");
|
||
println!(
|
||
"meter: {}",
|
||
if hardware {
|
||
format!("Sandbox PowerMeter — hardware ({})", pm.sources().join(", "))
|
||
} else {
|
||
let why = match sandbox_meter::ioreport_status() {
|
||
// Apple Silicon: say why the SoC counters were declined.
|
||
sandbox_meter::IoReportStatus::Unavailable => String::new(),
|
||
status => format!(" ({status})"),
|
||
};
|
||
format!(
|
||
"no hardware probe{why} — each interaction below is sealed with the estimator's \
|
||
wall-clock × TDP figure for its own span"
|
||
)
|
||
}
|
||
);
|
||
let meter = SpanMeter::new(pm);
|
||
|
||
let surface = ContentId::of_bytes(b"<Form><Slider action='render'/></Form>");
|
||
let signing = ed25519_dalek::SigningKey::from_bytes(&[5u8; 32]);
|
||
let mut log = InteractionLog::new(signing);
|
||
|
||
// Three real interactions: each handler does ~150ms of genuine CPU work
|
||
// (the "computation behind the UI"), metered by whatever the meter can read.
|
||
for n in 1..=3u64 {
|
||
let it = run_interaction(&surface, "render", "did:key:z6MkDemoUser", &n.to_le_bytes(), &meter, None, || {
|
||
let t = Instant::now();
|
||
let mut x = n;
|
||
while t.elapsed() < Duration::from_millis(150) {
|
||
for _ in 0..50_000 {
|
||
x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||
}
|
||
}
|
||
std::hint::black_box(x).to_le_bytes().to_vec()
|
||
});
|
||
println!(" render #{n}: {} · {}", it.verdict.tag(), figure(&it.energy));
|
||
log.record(it);
|
||
}
|
||
|
||
let verified = verify_chain(log.entries()).expect("chain verifies");
|
||
// Label the total by the sealed records, not by the meter: "measured"
|
||
// only when every record is a hardware reading that moved.
|
||
let records: Vec<&Energy> = log.entries().iter().map(|e| &e.interaction.energy).collect();
|
||
let unmetered = records.iter().filter(|e| e.uj() == 0).count();
|
||
let label = if unmetered > 0 {
|
||
format!(
|
||
" — {unmetered} of {} interaction(s) have no figure (0 µJ for work that ran): not a measurement",
|
||
records.len()
|
||
)
|
||
} else if records.iter().all(|e| e.provenance == Provenance::HwShunt) {
|
||
if hardware_in_process {
|
||
" — hardware counter deltas (HwShunt), this process's CPU energy over each interaction".to_string()
|
||
} else {
|
||
" — hardware counter deltas (HwShunt), machine-wide over each interaction".to_string()
|
||
}
|
||
} else if records.iter().all(|e| e.provenance == Provenance::Estimator) {
|
||
" — estimated (wall-clock × TDP over each interaction's span), not measured".to_string()
|
||
} else {
|
||
" — partly estimated, not measured".to_string()
|
||
};
|
||
println!("\nsealed chain: {verified} links verified, {} µJ total{label}", log.total_uj());
|
||
}
|