-
-
Notifications
You must be signed in to change notification settings - Fork 441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add cpufreq support on Darwin for Apple Silicon machines #1272
base: main
Are you sure you want to change the base?
Conversation
Nice.
I hope the source contains some documentation on the details of this.
Calling me surprised by this, in the context if this being apple, would be an overstatement …
I keep a firm distance from any fruit-based hardware, thus I cannot test this myself either. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please squash your changes into the commits of this PR to keep the patch set clean.
# ---------------------------------------------------------------------- | ||
# Checks for Darwin features and flags. | ||
# ---------------------------------------------------------------------- | ||
|
||
if test "$my_htop_cpu" = aarch64; then | ||
AC_ARG_ENABLE([libioreport], | ||
[AS_HELP_STRING([--enable-libioreport], | ||
[enable libIOReport support for getting cpu frequency on Apple Silicon machines @<:@default=yes@:>@])], | ||
[], | ||
[enable_libioreport=yes]) | ||
case "$enable_libioreport" in | ||
no) | ||
;; | ||
yes) | ||
AC_CHECK_LIB([IOReport], [IOReportCreateSubscription]) | ||
;; | ||
*) | ||
AC_MSG_ERROR([bad value '$enable_libioreport' for --enable_libioreport]) | ||
;; | ||
esac | ||
fi | ||
|
||
# ---------------------------------------------------------------------- | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably follow the pattern used for other libraries and include some auto-detection of library availability.
AM_CONDITIONAL([HTOP_AARCH64], [test "$my_htop_cpu" = aarch64]) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is handled consistently, as we currently don't handle different CPU architectures any different while on the some underlying OS/platform. There's no difference between Linux on ARM vs. Linux on i386 or x64.
Thus I'm not really in on adding this additional define. Thoughts @ @fasterit @natoscott @cgzones?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just use the predefined macro __aarch64__
to detect the CPU architecture, and combine with HTOP_DARWIN
defined by the configure script to get what we need?
#include <stdlib.h> | ||
#include <string.h> | ||
#include <stdint.h> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heder order, second blank after headers. Cf. styleguide.
#if (defined(HAVE_LIBIOREPORT) && defined(HTOP_AARCH64)) | ||
#define CPUFREQ_SUPPORT | ||
|
||
#include <IOKit/IOKitLib.h> | ||
#include <CoreFoundation/CoreFoundation.h> | ||
|
||
/* Private API definitions from libIOReport*/ | ||
enum { | ||
kIOReportIterOk = 0, | ||
}; | ||
typedef struct IOReportSubscriptionRef *IOReportSubscriptionRef; | ||
typedef CFDictionaryRef IOReportSampleRef; | ||
typedef CFDictionaryRef IOReportChannelRef; | ||
typedef int (^io_report_iterate_callback_t)(IOReportSampleRef ch); | ||
extern void IOReportIterate(CFDictionaryRef samples, io_report_iterate_callback_t callback); | ||
extern CFMutableDictionaryRef IOReportCopyChannelsInGroup(CFStringRef, CFStringRef, void*, void*); | ||
extern IOReportSubscriptionRef IOReportCreateSubscription(void *a, CFMutableDictionaryRef desiredChannels, CFMutableDictionaryRef *subbedChannels, uint64_t channel_id, CFTypeRef b); | ||
extern CFDictionaryRef IOReportCreateSamples(IOReportSubscriptionRef iorsub, CFMutableDictionaryRef subbedChannels, CFTypeRef a); | ||
extern uint32_t IOReportStateGetCount(IOReportChannelRef ch); | ||
extern uint64_t IOReportStateGetResidency(IOReportChannelRef ch, uint32_t index); | ||
extern CFDictionaryRef IOReportCreateSamplesDelta(CFDictionaryRef prev, CFDictionaryRef current, CFTypeRef a); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably all best kept to the implementation file instead of in the header.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of it is needed in the CpuFreqData
struct, most of it I can move to the implementation.
|
||
/* Definitions */ | ||
typedef struct { | ||
uint32_t num_frequencies; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should include stdint.h
in the header
for (size_t i = 0U; i < CPUFREQ_NUM_CLUSTER_TYPES; i++) { | ||
free(data->cpu_frequencies_per_cluster_type[i].frequencies); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this missing a follow-up call of free(data->cpu_frequencies_per_cluster_type);
???
@@ -76,6 +76,9 @@ void Machine_scan(Machine* super) { | |||
DarwinMachine_allocateCPULoadInfo(&host->curr_load); | |||
DarwinMachine_getVMStats(&host->vm_stats); | |||
openzfs_sysctl_updateArcStats(&host->zfs); | |||
#ifdef CPUFREQ_SUPPORT | |||
CpuFreq_update(&host->cpu_freq); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this check for this->cpu_freq_ok
if it's stored from init earlier?
|
||
#include "Machine.h" | ||
|
||
#if (defined(HAVE_LIBIOREPORT) && defined(HTOP_AARCH64)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#if (defined(HAVE_LIBIOREPORT) && defined(HTOP_AARCH64)) | |
#if defined(HAVE_LIBIOREPORT) && defined(HTOP_AARCH64) |
We're not writing Lisp …
CpuFreqData cpu_freq; | ||
bool cpu_freq_ok; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By refactoring CpuFreq_init
to return CpuFreqData*
on success and NULL
on failure, the bool
can be omitted.
if (dhost->cpu_freq_ok) { | ||
mtr->values[CPU_METER_FREQUENCY] = dhost->cpu_freq.frequencies[cpu - 1]; | ||
} else { | ||
mtr->values[CPU_METER_FREQUENCY] = NAN; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (dhost->cpu_freq_ok) { | |
mtr->values[CPU_METER_FREQUENCY] = dhost->cpu_freq.frequencies[cpu - 1]; | |
} else { | |
mtr->values[CPU_METER_FREQUENCY] = NAN; | |
} | |
mtr->values[CPU_METER_FREQUENCY] = dhost->cpu_freq_ok ? dhost->cpu_freq.frequencies[cpu - 1] : NAN; |
@bakaid how did you figure this out? Nothing is to be found about this API at all. Did you reverse engineer powermetrics? I'm interested in this myself as I wrote the macos (and open/freeBSD) code for btop. I'd like to use parts of this code, but as htop is GPL and btop is Apache licensed, this is problematic. Would you be open to dual license the cpu frequency bit? |
The only reference available for this is the behaviour of After this PR has been submitted, I finally had the opportunity to test this on an M2 Pro machine, and realized that unfortunately, this solution is incomplete. With Pro, Max and Ultra Apple Silicon CPUs, there is more than one P-cluster, and in the case of Ultra, more than one die. It seems to me as if there is a mode, where individual cores have their individual scaled frequencies, and a mode, where an entire cluster is driven with the same frequency, the latter especially prominent under larger loads. This "cluster-wide same frequency mode" is reported in another channel, The solution, it seems to me, is to merge these two datasets, and synthesize an average frequency for each core, based on the weighted average of its own individual This is what I plan to do, but haven't had the time (and reliable access to Pro/Max/Ultra machines) to do it yet. As for licensing, once this is finalized, I am happy to dual-license the code. |
@bakaid thanks for all the info! I only have an M1 and an M2 myself. I assumed this code would also work on x86, but apparently the IORegistry nodes Great that you are open to dual license your work! 🎉 🙏 |
@joske of course, happy to do it. Indeed, this solution is Apple Silicon-exclusive, Now that I see that there is interest in this, I'll try to make progress with the complex performance states. |
Btw, btop can only display a single frequency, so I was only showing the highest one. |
@bakaid any progress? |
This PR adds CPU frequency support on Darwin for Apple Silicon machines.
After doing research on this, it seems to me that the only way to get cpufreq data is to gather some information from the IODeviceTree using IORegistry and then get performance statistics using libIOReport. Unfortunately, the former's structure is not guaranteed to be stable, and the latter has a private API, so again, it can break at any update.
That being said, based on my research on powermetrics and the available device trees of Apple Silicon SoCs, the solution presented here seems to be the current best option available.
I tested this on vanilla M1 and M2, but not on Max/Pro (different core count) or Ultra (multi-die) CPUs. I am reasonably certain it should work on those as well, as we use cluster-type (E or P), not cluster-id for classifying cores, but someone with those CPUs testing this would be very helpful.
Resolves #280