Thermals and macOS
We build a lot of code for Apple platforms at Google, and spend a fair amount of time trying to optimize our builds. We have noticed that Macs are quite sensitive to thermals, and have done some work attempting to track the thermal characteristics of our machines and how that affects their build speeds. Here’s some info that we discovered that may be useful in your own explorations. The intended audience for this is someone quite familiar with macOS programming.
APIs
Apple has a plethora of APIs to monitor thermals on their platforms. Warning, your mileage may vary… Note also that I am focusing on userspace APIs. Kernel hackers have many more fun exciting and barely documented ways to get at thermal info.
IOPMGetThermalWarningLevel
IOPMGetThermalWarningLevel
appears to have been introduced in macOS 10.7 and is documented to return one of three values:
kIOPMThermalWarningLevelNormal
kIOPMThermalWarningLevelDanger
kIOPMThermalWarningLevelCrisis
The implementation for IOPMGetThermalWarningLevel
depends on SCDynamicStore
looking for the ‘/IOKit/Power/ThermalWarning
’ key. As far as I can tell, there is nothing that sets this key in System Configuration on macOS 10.14/10.15 so this call is useless (filed as Feedback FB7528845 — Update that this still appears to be broken on macOS 12.0.1).
IOGetSystemLoadAdvisory and IOCopySystemLoadAdvisoryDetailed
IOGetSystemLoadAdvisory
andIOCopySystemLoadAdvisoryDetailed
appear to have been introduced in macOS 10.6. IOGetSystemLoadAdvisory
returns a simple value that is not a thermal measurement, but takes thermals into consideration. IOCopySystemLoadAdvisoryDetailed
returns a dictionary containing the ‘kIOSystemLoadAdvisoryThermalLevelKey
’ key. This key appears to contain one of these values:
kIOSystemLoadAdvisoryLevelBad
= 1kIOSystemLoadAdvisoryLevelOK
= 2kIOSystemLoadAdvisoryLevelGreat
= 3
The implementation for IOCopySystemLoadAdvisoryDetailed
also depends on SCDynamicStore
, in this case looking for the ‘/IOKit/PowerManagement/SystemLoad/Detailed
’ key. Contrary to the ThermalWarning key above, this is set.
IOPMCopyCPUPowerStatus
Returns a dictionary various CPU attributes.
kIOPMCPUPowerLimitProcessorSpeedKey
CPU speed. This does get change when the machine gets hot and thermals come into play.kIOPMCPUPowerLimitProcessorCountKey
Apparently the number of processors that are currently active. Seecpuctl
below though for caveats.kIOPMCPUPowerLimitSchedulerTimeKey
Apparently controls job scheduling on CPUs have never seen it change, and not sure how to play with it.
NSProcessInfo
NSProcessInfo contains some useful thermal related functionality. You can register for the NSProcessInfoThermalStateDidChangeNotification
, which alerts you to when you should check [[NSProcessInfo currentProcess] thermalState]
for updates. -thermalState
is documented to return the following values:
NSProcessInfoThermalStateNominal
NSProcessInfoThermalStateFair
NSProcessInfoThermalStateSerious
NSProcessInfoThermalStateCritical
This notification does work on macOS 10.15 and has been around since macOS 10.10.3.
OSThermalNotification.h
The very hopeful sounding OSThermalNotification.h appears at first glance to only support Apple’s mobile platforms. If you look carefully though you will see kOSThermalNotificationPressureLevelName
which first appeared in macOS 10.10. It is just a string constant to be used with Darwin Notifications. See below for details.
Darwin Notifications [aka notify(3)]
The Darwin notification system has been around since macOS 10.3. It has a very detailed man page: man 3 notify
. There are a couple of notification strings scattered around the headers that may be useful.
The most promising notification in IOKit is kIOPMThermalWarningNotificationKey
. It’s likely that this was intended to be used to notify you when to call IOPMGetThermalWarningLevel
. Unfortunately this notification never gets sent (as far as I can tell).
Other possibly useful notifications from IOKit are kIOSystemLoadAdvisoryNotifyName
and kIOPMCPUPowerNotificationKey
which alert you when to check IOCopySystemLoadAdvisoryDetailed
and IOPMCopyCPUPowerStatus
respectively. Note that if you don’t want to get the system load details for kIOSystemLoadAdvisoryNotifyName
you can call notify_get_state
to get the same value as IOGetSystemLoadAdvisory
.
From OSThermalNotification.h
we have kOSThermalNotificationPressureLevelName
which returns one of the following values using notify_get_state
:
kOSThermalPressureLevelNominal
kOSThermalPressureLevelModerate
kOSThermalPressureLevelHeavy
kOSThermalPressureLevelTrapping
kOSThermalPressureLevelSleeping
As it turns out this is what NSProcessInfoThermalStateDidChangeNotification
uses under the hood, and the constants appear to map:
kOSThermalPressureLevelNominal
=NSProcessInfoThermalStateNominal
kOSThermalPressureLevelModerate
=NSProcessInfoThermalStateFair
kOSThermalPressureLevelHeavy
=NSProcessInfoThermalStateSerious
kOSThermalPressureLevelTrapping
=NSProcessInfoThermalStateCritical
Notifications are sent through /usr/sbin/notifyd
and are posted by /usr/libexec/thermald
.
Note: Another useful notification along these same lines is sent out when Low Power mode is activated via the “Battery” preference panel on a MacBook. The notification is is `com.apple.system.lowpowermode`. There doesn’t appear to be an official public constant defined for it (FB9742239). The state (on/off) can be determined using notify_get_state.
Testing
So how can we test macOS thermal code? (short of wrapping your machine up in a jacket…which we did for a while). With Xcode 11.3 Apple has introduced ways to force thermal profiles on iOS devices, but nothing exists for the Mac.
It turns out that there are a variety of command line utilities that will give you information about thermals:
powermetrics
Returns a huge amount of information about power and thermals. Probably overkill in most cases.
cpuctl
A little known (as far as I can tell) command that lets you control the number of processors available at any given time. It has a man page, and from what I can tell does actually turn processors off. You can see it using Activity Monitor and its pretty CPU graphics. The Instruments.app preferences will also tell you that CPUs are disabled (and is another way of disabling CPUs).
That being said, turning off a processor via either cpuctl
or instruments
does not appear to send a notification via kIOPMCPUPowerNotificationKey
regarding a CPU being disabled (FB9738489–macOS 11.6 and 12.01)
pmset -g thermlog
Sits waiting on notifications via kIOPMThermalWarningNotificationKey
and kIOPMCPUPowerNotificationKey.
Since thermal notifications via kIOPMThermalWarningNotificationKey
never get sent and CPU count never changes, this is mainly only good for getting a continuously updated number for CPU Speed limit.
thermal
thermal
does not have a man page, but running it without any arguments gives some basic “help”. There’s some real goodness in here allowing you to simulate thermal pressure notifications, get current levels and get the thermal configuration for your machine. It turns out that these configurations are all stored in /System/Library/Extensions/IOPlatformPluginFamily.kext/Contents/PlugIns/X86PlatformPlugin.kext/Contents/Resources/
and are keyed by a per motherboard UUID. The configurations define the levels at which notifications are going to get sent.
For what it’s worth I have never been able (willing) to force my machines to report any real value higher than kOSThermalPressureLevelModerate
which is pretty surprising given that I have gotten my CPU Speed Limits down below 50. As mentioned above I had my MacBook Pro 16" doing a full build of YouTube while running two YouTube videos while wrapped in a fleece jacket, and that wasn’t sufficient to push the thermal levels above 120 (according to thermal levels)
. My fans were going full bore, I was afraid of doing damage, and my office mates were getting annoyed. According to Apple at WWDC 2015 you only need to worry once you reach NSProcessInfoThermalStateSerious
and above, but in my experience so far you are unlikely to ever reach these levels.
Conclusions
Use notify with kOSThermalNotificationPressureLevelName
(or the higher levelNSProcessInfoThermalStateDidChangeNotification
). Test using thermal
.