Why four fifths of your Mac's CPU energy has no owner
Updated 23 August 2026 · measured on an M3 Max on 30 July 2026. This one is written for people building the same kind of tool. Who measures this.
macOS will tell you how much energy a process has used. The counter is cumulative, it is cheap to read, and it dies with the process. Anything born and finished between two samples was never in any snapshot, and on an idle Mac that turns out to be most of the work.
The counter and its hole
Per-process attribution starts at
proc_pid_rusage(pid, RUSAGE_INFO_V6).ri_energy_nj. Sample every ten
seconds, diff by matching pid, and you have energy per process. What you do not have
is anything that lived and died inside those ten seconds.
Measured, summing every live process against the CPU energy channel:
| Load | Sum over live processes | Share of CPU energy channel |
|---|---|---|
| Idle machine | 7.1 to 8.4 W | 20 to 23% |
| Building an app | 14.9 W | 64% |
On the quiet machine four fifths of processor energy had no owner. A Swift build
makes the mechanism obvious: 161 distinct swift-frontend pids
across 164 windows. Every one of them compiled something, used power and
was gone before the next sample. All of it landed in a row labelled "system, no
owner".
The ledger that outlives the task
XNU groups tasks into a coalition: the application, its helpers, its XPC services, its forks and every descendant. The coalition carries a ledger, and when a task dies its energy stays in that ledger.
Nothing about this is private knowledge.
powermetrics --show-process-coalition prints the same figures from the
same syscall, and Chrome calls it in shipping builds. No root, no entitlement. There
is no public header, so the three calls go through dlsym:
| Call | What it gives |
|---|---|
proc_listcoalitions(1, 0, buf, sz) | Every resource-type coalition, including the empty ones |
coalition_info_resource_usage(cid, buf, 512) | The ledger itself |
proc_pidinfo(pid, 20, …) | Which coalition a pid belongs to |
The empty coalitions are the whole point
The obvious way to enumerate coalitions is to walk live pids and collect the coalitions they belong to. Chromium does exactly that, and it loses precisely the case that matters: an application whose every process has already exited.
Counted on a live machine: 1600 coalitions, of which only 729 had a live task. The other 871 carry the energy of processes that no longer exist. Enumerate through live pids and that energy stays invisible.
What to do with the remainder
Per coalition, per tick: take the ledger delta, subtract the sum of deltas from its own live members, and what remains belonged to tasks that died. Give it to the coalition leader rather than splitting it across members, because the work was caused by the application as a whole and belongs in its row.
Two rules keep that honest. A coalition seen for the first time cannot be booked whole: its counter has been running since before you were watching, so the first sighting sets a baseline and books nothing. And the leader has to be chosen deliberately, not taken as whichever member happened to be enumerated first.
What it changed
| Measure | Before | After |
|---|---|---|
| Share of CPU energy with a name | 20 to 23% | 91% |
| "CPU, no owner" row on battery | most of the channel | 1.3 to 1.9 W |
| Named pool coming from the ledger | — | 86 to 89% |
The cost is small enough to state precisely: proc_listcoalitions takes
0.089 ms, reading 1600 ledgers takes 2.0 ms. About 2 ms per tick against the 0.95 ms
already spent walking proc_pid_rusage. At a ten second tick that is
0.02% of one core, and it wakes no new timer.
The rule this broke
The old invariant was that a row never receives more energy than its own counter recorded. That rule is what produced the hole: it is exactly correct per process and exactly wrong per application, because an application is not one process. Chrome held a median of 16 processes on one Mac and 19 on another, peaking at 40.
Graphics energy has the same shape of problem and no such fix. macOS gives a total for GPU energy and no owner for it, so any split between apps is inferred from GPU time rather than measured. We say so on every page that prints a number. How we count has the rest of the method, including the parts that stay unsolved.
Related: what each app actually costs, measured · watts by workload · what macOS means by "using significant energy" · all the guides