HwMeter passed the family meter's whole-host estimators straight through.
Those accrue by the millisecond, so a span shorter than one — a served
request, here — read 0 µJ: on a host with no live whole-request counter
(this M5 Max), every request served with `server,hw-meter` or
`server,hw-meter-apple` sealed 0 µJ [Estimator], against HwMeter's own
rule that a span is never billed a zero. A backend that is a whole-host
estimate is now billed through this crate's EstimatorMeter, which
resolves a microjoule, and the bridge names itself `estimator` then.
ProcessMeter (`server,metal`) sealed ~1.5–9.5 J [Estimator] per request
for ~11 µs of engine work. The process CPU count does not advance over
so short a serve, so ProcessSpanMeter falls back to the host meter,
whose window opens before its first GPU-rail snapshot and closes after
its last — 100 ms or more of the meter's own waiting at each end — and
its load estimate billed that. A whole Estimator span (only that
fallback gives one) is now billed this crate's estimate over the work's
own window. The estimator's window for the rest of the host now opens
after the span meter's starting readings, the same window as the CPU
reading, instead of before its GPU snapshot wait.
Measured here, one served request: hw-meter 0 → 103 µJ, hw-meter-apple
0 → 113 µJ, metal 9,480,000 → 168 µJ, all [Estimator].
Tests (each fails before): an_estimator_backend_never_bills_a_short_
span_nothing (50 spans of 50 µs over the family EstimationMeter);
an_estimated_fallback_span_is_billed_the_works_own_window (energy_of over
the fallback span seen here); serve_example asserts usage.energy.uj > 0
for every grade.
The waits sit in jouleclaw-energy's ProcessSpanMeter (its fallback reads
bracket the GPU snapshots); that is not changed here.
Co-Authored-By: Claude Opus 5.5 <[email protected]>