Add live per-app power to the Today view#4
Conversation
The popover's Today range is now a hybrid: apps currently drawing power show live watts (sampled every 2 s while the popover is open), and the rest of today's history shows accumulated watt-hours below. The Stats window gets the same treatment with an extra live-watts column. Live power comes from per-coalition cumulative energy counters (CPU, GPU, and ANE, in nanojoules) read by the root helper through the coalition_info resource-usage syscall - the same accounting Activity Monitor uses. The helper ships raw counters over a new protocol v3 XPC method; the app differences consecutive snapshots into watts, smooths them with a half-life EMA, and keeps row order stable with rank hysteresis plus a 30 s idle grace period. Coalitions roll up to their outermost .app bundle so browser and Electron helpers attribute to the parent app; unattributable processes land in a labeled system bucket rather than being spread across apps. On battery, an attribution footer reconciles per-app watts against the battery draw instead of pretending they sum. Older helpers degrade to plain history with a caption.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab28188701
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let byteCount = Int32(pids.count) * Int32(MemoryLayout<Int32>.size) | ||
| let written = proc_listallpids(&pids, byteCount) | ||
| guard written > 0 else { return [] } | ||
| let count = Int(written) / MemoryLayout<Int32>.size |
There was a problem hiding this comment.
Stop dropping most PIDs from live snapshots
proc_listallpids already returns a count of PIDs, not a byte count, so dividing the second return value by MemoryLayout<Int32>.size keeps only the first quarter of the process list. On a typical machine with hundreds of processes, any app whose PID lands after that truncated prefix is never grouped into a coalition, so the new live per-app power view will silently miss active apps and underreport watts.
Useful? React with 👍 / 👎.
| return errno != 0 ? errno : -1; | ||
| } | ||
| out->coalitionID = info.coalition_id[JUICE_COALITION_TYPE_RESOURCE]; | ||
| out->isLeader = (info.coalition_role[JUICE_COALITION_TYPE_RESOURCE] == JUICE_COALITION_TASKROLE_LEADER) ? 1 : 0; |
There was a problem hiding this comment.
Do not read coalition roles from reserved fields
The PROC_PIDCOALITIONINFO result exposes coalition IDs plus reserved fields; it does not include per-task coalition roles. Reading info.coalition_role[0] therefore reads zeroed/reserved data, so membership.isLeader is effectively always false and LiveEnergyReader falls back to the lowest PID in each coalition. For multi-process coalitions where the leader is not the lowest PID, live attribution can pick the wrong executable path and roll watts up to the wrong app or the system bucket.
Useful? React with 👍 / 👎.
proc_listallpids returns a PID count from both the sizing and the fill call (unlike proc_listpids, which returns bytes). Dividing the fill result by the element size scanned only a quarter of the process table, so most coalitions were represented by whichever of their PIDs happened to land in that first quarter - or missed entirely. Resource coalitions also have no leader role in the kernel, so the reader now picks the lowest PID as the coalition's representative outright.
The struct is 40 bytes: one coalition id per type (resource, jetsam) plus three reserved words. The previous oversized declaration read its 'role' field from zeroed padding, so the leader-role check could never fire and the shim's leader flag was meaningless. Drop it.
The live model's idle fold hid sub-threshold apps from the reading, so the hybrid view's 30 s grace-period holdovers froze at their last visible wattage. The controller now runs the model with no idle fold - the merger applies the display threshold itself - and the model prunes apps that have decayed below a tenth of a milliwatt so a long session with app churn cannot grow state without bound. Also skip status updates from an already-cancelled sampling loop so an in-flight failure cannot overwrite the state a stop() just reset.
Only the earlier section was capped, so a machine with many apps above the active threshold could render arbitrarily many live rows and push the popover off screen again. Active rows now spend the same 8-row budget first, and overflow from either section lands in the fold row.
Active-row overflow was combined with history overflow into a single fold row under Earlier Today, miscategorizing currently-active apps as history (and summing their watt-hours, not their watts). Each section now folds its own overflow: the live section in watts, the history section in watt-hours, still within the 8-row budget.
Metadata was restored after the prune removed it, so an app that decayed below the prune floor and then exited left its metadata behind forever. Refreshing before the smoothing loop lets the prune reclaim both maps together.
What
The popover's Today range becomes a hybrid view: a Drawing power now section with live per-app watts (sampled every 2 s while the popover is open, values in green with today's Wh as subtext), above an Earlier today section with the usual watt-hour ranking. The Stats window's Today gets the same two sections with an extra live-watts column. Ranges are Today / 3 Days / Week / All ("All Time" shortened to fit).
Why
macOS meters per-app energy continuously, but Juice could only show history - the "Drawing 12.4 W" headline had no per-app breakdown. This answers "what is eating my battery right now" in the same place users already look.
How
Verified
make test), including new unit suites for the power model (delta math, counter resets, coalition reappearance, hysteresis, idle fold, .app rollup) and the live/today merger (threshold, grace period, ordering).