# What a counter can actually resolve Two harnesses behind the numbers `jcp-energy` declares. Both are the evidence for a constant in the crate; neither is a test of the crate. ## `counter_resolution.py` — the per-process CPU counter's update period `ri_energy_nj` is the only source in the survey that attributes energy to a *principal* rather than to silicon, so whether it may grade a span decides whether Apple silicon has any process-attributed reading at all. Burns this thread's CPU for a fixed interval, samples the counter 60 times per interval, and reports the fraction of intervals in which it advanced — the crate's own liveness criterion is 80%. Measured on an M5 Max, macOS 27: | interval | advanced | median step | clears 80% | |---:|---:|---:|---| | 10 µs | 0% | — | no | | 50 µs | 0% | — | no | | 100 µs | 2% | 20.4 mJ | no | | 500 µs | 5% | 21.1 mJ | no | | 1 ms | 35% | 2.8 mJ | no | | 2 ms | 63% | 4.9 mJ | no | | **5 ms** | **87%** | 9.6 mJ | **yes** | | 10 ms | 100% | 19.0 mJ | yes | | 50 ms | 100% | 92.9 mJ | yes | The crate declares **10 ms**, not the 5 ms where it first clears the criterion: over-declaring a period refuses spans that would have been fine, while under-declaring grades spans that were not, and at a ceiling only the second error is dangerous. Note the shape below 1 ms. The counter does not lag smoothly — on the few intervals where it advanced at all, the median step was ~20 mJ, two orders above the 1 ms step. It accrues in lumps, which is the same batched-refresh pathology the crate already refuses on IOReport's CPU rails. A sampler that read it every 500 µs would see nothing most of the time and a 20 mJ jump occasionally, and would report both as measurements. This is also why a per-operation figure for an 11.85 ms decision is taken as the mean of 200 decisions over a 2.6 s span rather than as 200 counter reads. ## `ioreport_rails.py` — which Apple rails exist and advance Enumerates the IOReport `Energy Model` group unprivileged (364 rails on an M5 Max) and prints each channel's name, unit and delta. What it establishes: `GPU Energy` is live in **nJ**, every `CPU Energy` / `MCPU*` / `PACC*` / `*_SRAM` rail is frozen for an unprivileged reader, and `ANE0` is the only ANE channel — no `ANE Energy` aggregate, no sub-rails. Units differ *within one sample*, so a summer that assumes one unit is wrong. ## `gpu_rail_period.py` / `gpu_rail_idle_tail.py` — when the GPU rail republishes The `GPU Energy` rail has **no fixed period**. It republishes when there is activity to report and holds its value when there is not, so the number you measure depends entirely on which condition you measure in. Sampling every 2 ms on an M5 Max, macOS 27, the gap between actual changes: | condition | p50 | p90 | p95 | p99 | max | n | |---|---:|---:|---:|---:|---:|---:| | sustained MLX matmul | 13.1 ms | 15.0 ms | — | — | **42.0 ms** | 435 | | idle, 6 s | 12.7 ms | 91.0 ms | — | — | 992.2 ms | 112 | | **idle, 90 s** | 13.1 ms | 30.6 ms | 86.2 ms | 436.2 ms | **2032.8 ms** | 2936 | Three things follow, and the third is why the long run exists. **Under sustained load the rail is tight; the long tail is an *idle* property.** That is the opposite of the intuitive reading, and it reconciles an earlier fleet observation that put the hold at "300–600 ms under load": the GGUF decode it came from is dispatch-latency-bound with idle gaps between tokens, so it was sampling idle behaviour. 300–600 ms is about the idle p98–p99 here, consistent with having seen that region and not past it. **A declared period has to be the idle worst case.** A measurer cannot guarantee the fabric stays busy, and a span landing entirely inside one hold reads **zero** — which a naive grader would report as a measurement of zero. `jcp-energy` declares 1 s for this rail on that basis. **The sample max is not a bound.** The 6 s run gave a max of 992.2 ms, which sits just inside a 1 s timeout and invites the conclusion that 1 s is enough. Over 90 s the same host produced **three gaps past 1000 ms — 1446, 1919 and 2033 ms** — so roughly 1 idle gap in 1000 exceeds a one-second wait, and the worst was 2× it. Any code that waits a bounded time for this rail to republish should bound above ~2.5 s, or treat the timeout as a counted outcome rather than a rare surprise.