Guide · Developer notes3 min read

PerfPowerServices at 190% CPU on macOS: the process-count trigger and a fix without rebooting

If /usr/libexec/PerfPowerServices is sitting at the top of Activity Monitor at 100% to 190% CPU and stays there for days, you are probably looking at the same thing I chased for two weeks on an M3 Max MacBook Pro. It is not an app. It is Apple’s power-logging daemon stuck in a retry loop, and on my machine the trigger is the total number of processes crossing about 2,700. Once you know that, you can clear it in ten seconds without a reboot.

The symptom

  • PerfPowerServices at 60% to 190% CPU, for days, fans on.
  • In top, its BSD syscall counter is frozen at 2,147,483,647 (the 32-bit maximum) with only 7 threads.
  • Memory stays around 16 MB, so it hides behind big apps in Activity Monitor’s default view.
  • macOS writes its own report every time it happens: /Library/Logs/DiagnosticReports/PerfPowerServices_<date>.cpu_resource.diag. Every stack in it ends in memorystatus_control.

Check it with:

ps -o pid,etime,time,%cpu -p $(pgrep -x PerfPowerServices)
ps -ax | wc -l
ls -t /Library/Logs/DiagnosticReports/PerfPowerServices_*

What it is not

I quit VS Code, iStat Menus, ChatGPT, Safari, Teams, WhatsApp and Edge at various points, and each one looked like the fix for a minute, because the daemon’s CPU swings between 20% and 190% on its own. With nothing open but Finder and a terminal, it held 175% to 190% for a full three minutes. Don’t bother hunting for the app.

What the trace shows

A one-second syscall trace of the daemon (root required):

sudo ktrace trace -f S0x040c -p $(pgrep -x PerfPowerServices) -T 1

It captured 89,871 events, all the same four lines, about 22,500 times a second:

  1. memorystatus_control command 1 (get the process priority list) with a null buffer: a size query.
  2. Returns 0 and a size: 23,016 bytes, which is 959 entries of 24 bytes, matching the live process count.
  3. The same call again with a buffer at a fixed address.
  4. Returns EINVAL. Back to step 1.

The same pair of calls from a root shell succeeds every time. The daemon never gives up and never backs off, so it spins until something kills it.

The ceiling

The kernel refuses that request when the buffer is larger than 65,536 bytes (65,536 works, 65,537 does not). At 24 bytes per process, that is about 2,730 processes. The morning this last happened, my Mac had 3,053 processes: three booted iOS simulators, a macOS VM, Xcode, VS Code, and a dozen Electron and Chromium apps. Nothing unusual for a development machine, and enough. The instance of the daemon that got stuck was started while the count was that high, and Apple’s own report shows it over the CPU limit from its first minute.

The part I can’t explain yet: after I closed everything and the count dropped to about 950, the stuck instance stayed stuck. Something in its state outlives the spike. I’m filing this with Apple Feedback and will add the number here.

The fix

Order matters. A restart while the count is still high gives you a new instance that is stuck within seconds.

  1. Get the process count under about 2,500. Shut down simulators you are not using, quit the VM, quit the Chromium apps.

    ps -ax | wc -l
  2. Kill the daemon. launchd restarts it immediately, because KeepAlive is set in its job.

    sudo kill $(pgrep -x PerfPowerServices)
  3. Watch it for a few minutes. Healthy is under 3% CPU.

On my machine the replacement ran at 0.1% to 2.5% over the next five minutes and stayed there.

Things that do not work:

  • sudo launchctl kickstart -k system/com.apple.PerfPowerServices is refused with “Operation not permitted while System Integrity Protection is engaged”. A plain kill is allowed.
  • Closing apps after it is stuck.
  • A reboot does work. It is just the slow version of the same two steps.

Keeping the count down

ps -ax | wc -l above 2,500 is the warning. CPU and memory tell you nothing; this machine has 128 GB and half of it was free. The expensive things on a development Mac are each booted simulator, the VM, and Chromium-based apps at 10 to 30 processes each. Mounted simulator runtime images also leave diskimagesiod helpers behind after shutdown, 36 of them on my machine.

Versions: macOS 27.0.1 (26A434), PerfPowerServices 3486.1.2, MacBook Pro with M3 Max.

Gavi Narra ·

All guides →