Skip to content

Show what suspend costs on a Mac, and add a power report - #25

Open
maralcbr wants to merge 3 commits into
mainfrom
feat/sleep-power-report
Open

maralcbr wants to merge 3 commits into
mainfrom
feat/sleep-power-report

Conversation

@maralcbr

Copy link
Copy Markdown
Collaborator

Lid-closed suspend on Apple Silicon is s2idle only and draws about 2 W on an M1 Pro, so a battery is flat in about a day, and nothing tells the owner. This makes the cost visible. It does not change how the Mac sleeps; the kernel side is separate work (maralcbr/omarchy-mx-mac#281, ticket 18).

What changes

  • omarchy power report (omarchy-mac/bin/omarchy-power-report) prints:
    • battery charge and draw now, and each Mains and USB-C power source
    • kernel and power profile (it notes that power-profiles-daemon's placeholder driver changes nothing)
    • per-policy cpufreq range and limits, cpuidle driver and states
    • whether apple_pmp is bound, mem_sleep, and the last sleep's cost
    • a 10 s CPU activity sample, given as a share of one core and never as watts
  • System-sleep hook (/usr/lib/systemd/system-sleep/omarchy-mac-sleep-cost → lib/sleep-cost pre|post). It records energy_now, status, charger state, boot ID, CLOCK_REALTIME and CLOCK_BOOTTIME around every sleep, and writes one line per sleep to /var/lib/omarchy-mac/sleep-cost.log (newest 200 kept).
    • The duration comes from the boot clock and must agree with the wall clock.
    • Not counted: a charger online or charging at either end, energy that rose, a missing reading, or disagreeing clocks.
    • It catches its own errors, so it never fails a sleep.
  • User unit omarchy-sleep-cost.service, enabled for every user like the audio watchdog. It follows logind's PrepareForSleep. After a resume it waits for the unlock, then shows "Suspend used X Wh over Y h. Overnight suspend can substantially drain this Mac." once. Every counted sleep since the last unlock is added up, and the notice needs 30 minutes or more.
  • Manual: a new Battery and sleep page (limits, shut down for long unplugged periods, sleep mode battery improvement AsahiLinux/linux#262, how to opt out). The hardware page now splits power profiles into a known issue. Upstream and Status move from 13/14 to 14/15; slugs and URLs are unchanged.
  • install stages vendor/systemd/system-sleep/* as 0755.

Needs a maintainer's yes

  • The notice is on for every user by default. Opt out with systemctl --user mask --now omarchy-sleep-cost.service.
  • The hook runs as root on every Mac. systemd 262 reads hooks only from /usr/lib/systemd/system-sleep, so an /etc mask is not possible; the manual documents pacman NoExtract.
  • The sleep log is world-readable, because the user unit reads it.
  • omarchy-mac/version stays at 0.1.0. Nothing is tagged yet, and no behaviour PR since 0.1.0 has bumped it.

Test plan

  • omarchy-mac/test/power-report-test.sh: fixture M1 Pro sysfs and /proc. Covers charger-hidden draw, a bound and an unbound PMP, the ranking in the CPU sample, and guest time not counted twice.
  • omarchy-mac/test/sleep-cost-test.sh: one valid sleep, plus each rejection: charging, Mains or USB online, energy rose, a missing reading, no pre-sleep reading or one from another boot, clock disagreement. Also short and stale sleeps, the wait for the unlock, adding up a sleep taken before the unlock, an unknown lock state, and retrying a failed send.
  • Full omarchy-mac/test/all and shellcheck in an Arch container. platform-test.sh run natively on the M1 Pro, because under Rosetta it fails on clean main too (a .cache/rosetta directory).
  • On the M1 Pro (linux-aurora 7.1.12):
    • the report reads every path correctly
    • sleep-cost pre/post rejects a real interval as charger-connected
    • the notice renders on the desktop
    • dbus-monitor runs as the user (it falls back to eavesdropping, as the runtime's sleep monitor does)
  • Not yet run: a real lid-closed suspend on battery, to confirm energy_now and CLOCK_BOOTTIME right after resume. The journal shows the monotonic clock stops during s2idle on this kernel, which is why the duration is cross-checked against the wall clock.

Lid-closed s2idle draws about 2 W on an M1 Pro, enough to empty the battery in
about a day, and nothing told the owner. A system-sleep hook now records the
battery's energy and both the wall and boot clocks around every sleep, rejecting
intervals with a charger, charging, a gain, a missing reading or disagreeing
clocks. A user unit shows what a counted sleep of 30 minutes or more used at the
next unlock. omarchy power report gathers the draw, power sources, profiles,
cpufreq limits, cpuidle states, PMP binding, mem_sleep, the last sleep and a CPU
activity sample. The manual gains a Battery and sleep page.
A short sleep before the morning unlock replaced the overnight record, so the
notice never showed it. The notice now adds up every counted sleep since the
last unlock, waits at most two minutes on an unreadable lock state, and retries
a failed send. The hook catches its own errors, bounds the platform check and
keeps the pending reading until the log is written. The report stops counting
guest time twice and asks to unplug only while a charger is online. The manual
says how to turn the notice and the recording off.
A notice that failed every send is now tried again at the next resume instead
of being marked shown. A battery that is discharging while a USB-C source is
online shows its draw rather than asking to unplug. The opt-outs say to stop the
running watcher and where NoExtract goes.
@maralcbr
maralcbr requested a review from scottjones as a code owner October 10, 2026 00:28

@scottjones scottjones left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Marcelo, this is really useful. Showing people what suspend costs is the right move while the kernel side gets sorted, and the power report is great to have.

Your "Needs a maintainer's yes" items: yes to all three: the notice on by default, the root hook in system-sleep, and the world-readable sleep log. We can revisit the notice once the sleep drain itself is fixed.

Tested on hardware: 14" M2 Max (J414c), on battery, with #25 staged by its own install script, the hook in system-sleep and the watcher running as a user unit.

  • A 34.7-minute s2idle suspend (10:05:38 to 10:40:17 in the journal) was recorded as "used 0.8 Wh over 0.6 h (1.30 W average)".
  • After I unlocked, the notice "Suspend used 0.8 Wh over 0.6 h." appeared.
  • The boot clock and the RTC agreed (2101 s vs 2102 s).
  • omarchy-power-report printed every section, including the last suspend.

That covers the clock half of the item still open in your test plan. The energy half is where I'd look next.

The undercount, confirmed: my own energy_now readings around that suspend gave at least 1.22 Wh (my reading 8 s after resume was taken before the gauge settled) against the 0.75 Wh #25 recorded, so I ran a short test to see why. Same Mac, on battery, reading energy_now and power_now every 2 s across a 334 s (5.6 min) systemctl suspend.

  • Before the suspend, energy_now fell in steady 11400 µWh steps at about 13 W.
  • The first reading after resume, right as the kernel logged "PM: suspend exit", was 18525000 µWh. That's about 0.50 Wh higher than the last reading before the suspend, so a reading taken then says the Mac gained energy while asleep.
  • The gauge then caught up in big steps (148200 µWh twice, then about 80000 at a time) for about 35 s, before going back to normal 11400 steps. One more ~91000 step came at 86 s. Beyond what power_now says was used while awake, it dropped an extra 0.71 Wh by 34 s and 0.80 Wh by 86 s.
  • From the settled reading minus the awake use, the sleep itself cost 0.29 Wh, about 3.1 W. That's higher than the longer run's rate, which fits a short suspend paying a fixed cost to go in and come out.

So the SMC gauge reports a stale, too-high energy_now right after resume and catches up over the next 30 to 90 s. record_post reads it from the system-sleep hook at the moment of resume, so it undercounts. On a short suspend it's worse: the reading comes out higher than before the sleep, so judge() returns energy-rose and the sleep is dropped without a record.

Suggestion: take the post-sleep reading once the gauge has settled. For example, keep sampling until energy_now has moved in normal steps for a while, or just wait about 90 s, then subtract the awake use: power_now integrated over that time, or the delay times the awake draw. Or take it at unlock and subtract what was used while awake since the resume.

Smaller things:

  • The comment at lib/sleep-cost:17-18 claims more than the BOOTTIME/REALTIME cross-check can catch. Both clocks get the same sleep delta on resume, so it only catches a wall-clock step, not a boot clock that missed the sleep. My run shows it's fine in practice, so this is just a wording fix.
  • The platform gates have no inverse tests. Removing the gate in omarchy-power-report (around line 195) or in lib/sleep-cost (around line 276) still passes both test files.
  • Under dbus-broker, dbus-monitor logs "unable to enable new-style monitoring ... Falling back to eavesdropping". It still got PrepareForSleep, and I know the runtime's sleep monitor does the same. A plain signal subscriber such as gdbus monitor --system --dest org.freedesktop.login1 needs no monitoring rights, though. Optional.
  • The PR body says the version "stays at 0.1.0". Once #30 lands no bump is needed at all, so that line can just go.

The post-resume reading is the one thing I'd want fixed before this merges. The rest is small.

@maralcbr

Copy link
Copy Markdown
Collaborator Author

Maintainer decisions on the three policy points:

  1. The post-unlock sleep-cost notice is on by default for every user, with the documented opt-out.
  2. The root sleep hook stays unmaskable from /etc; pacman NoExtract is the documented way to disable it.
  3. The sleep log stays world-readable (it holds battery Wh figures only).

Still open before merge: one real lid-closed suspend on battery to confirm the energy and boot-clock readings after resume.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants