Skip to content

Commit

Permalink
Add cpufreq support on Darwin for Apple Silicon machines
Browse files Browse the repository at this point in the history
  • Loading branch information
bakaid committed Jul 26, 2023
1 parent 5558c01 commit cba2a28
Show file tree
Hide file tree
Showing 7 changed files with 307 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ darwin_platform_headers = \
darwin/DarwinMachine.h \
darwin/DarwinProcess.h \
darwin/DarwinProcessList.h \
darwin/CpuFreq.h \
darwin/Platform.h \
darwin/PlatformHelpers.h \
darwin/ProcessField.h \
Expand All @@ -353,6 +354,7 @@ darwin_platform_sources = \
darwin/DarwinMachine.c \
darwin/DarwinProcess.c \
darwin/DarwinProcessList.c \
darwin/CpuFreq.c \
generic/fdstat_sysctl.c \
generic/gettime.c \
generic/hostname.c \
Expand Down
38 changes: 38 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ AC_USE_SYSTEM_EXTENSIONS

# ----------------------------------------------------------------------

# ----------------------------------------------------------------------
# Checks for cpu.
# ----------------------------------------------------------------------

case "$host_cpu" in
aarch64*)
my_htop_cpu=aarch64
AC_DEFINE([HTOP_AARCH64], [], [Building for aarch64.])
;;
esac

# ----------------------------------------------------------------------

# ----------------------------------------------------------------------
# Checks for compiler.
Expand Down Expand Up @@ -678,6 +690,30 @@ fi
# ----------------------------------------------------------------------


# ----------------------------------------------------------------------
# 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

# ----------------------------------------------------------------------

# ----------------------------------------------------------------------
# Checks for compiler warnings.
# ----------------------------------------------------------------------
Expand Down Expand Up @@ -769,6 +805,8 @@ AM_CONDITIONAL([HTOP_SOLARIS], [test "$my_htop_platform" = solaris])
AM_CONDITIONAL([HTOP_PCP], [test "$my_htop_platform" = pcp])
AM_CONDITIONAL([HTOP_UNSUPPORTED], [test "$my_htop_platform" = unsupported])

AM_CONDITIONAL([HTOP_AARCH64], [test "$my_htop_cpu" = aarch64])

AC_SUBST(my_htop_platform)
AC_CONFIG_FILES([Makefile htop.1])
AC_OUTPUT
Expand Down
176 changes: 176 additions & 0 deletions darwin/CpuFreq.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#include "darwin/CpuFreq.h"

#ifdef CPUFREQ_SUPPORT

#include <stdlib.h>
#include <string.h>
#include <stdint.h>

/* Implementations */
int CpuFreq_init(const Machine* machine, CpuFreqData* data) {
data->existingCPUs = machine->existingCPUs;
data->cluster_type_per_cpu = xCalloc(data->existingCPUs, sizeof(uint32_t));
data->frequencies = xCalloc(data->existingCPUs, sizeof(double));

/* Determine the cluster type for all CPUs */
char buf[128];

for (uint32_t num_cpus = 0U; num_cpus < data->existingCPUs; num_cpus++) {
snprintf(buf, sizeof(buf), "IODeviceTree:/cpus/cpu%u", num_cpus);
io_registry_entry_t cpu_io_registry_entry = IORegistryEntryFromPath(kIOMainPortDefault, buf);
if (cpu_io_registry_entry == MACH_PORT_NULL) {
return -1;
}

CFDataRef cluster_type_ref = (CFDataRef) IORegistryEntryCreateCFProperty(cpu_io_registry_entry, CFSTR("cluster-type"), kCFAllocatorDefault, 0U);
if (cluster_type_ref == NULL) {
IOObjectRelease(cpu_io_registry_entry);
return -1;
}
if (CFDataGetLength(cluster_type_ref) != 2) {
CFRelease(cluster_type_ref);
IOObjectRelease(cpu_io_registry_entry);
return -1;
}
UniChar cluster_type_char;
CFDataGetBytes(cluster_type_ref, CFRangeMake(0, 2), (uint8_t*) &cluster_type_char);
CFRelease(cluster_type_ref);

uint32_t cluster_type = 0U;
switch (cluster_type_char) {
case L'E':
cluster_type = 0U;
break;
case L'P':
cluster_type = 1U;
break;
default:
/* Unknown cluster type */
IOObjectRelease(cpu_io_registry_entry);
return -1;
}

data->cluster_type_per_cpu[num_cpus] = cluster_type;

IOObjectRelease(cpu_io_registry_entry);
}

/*
* Determine frequencies for per-cluster-type performance states
* Frequencies for the "E" cluster type are stored in voltage-states1,
* frequencies for the "P" cluster type are stored in voltage-states5.
* This seems to be hardcoded.
*/
const CFStringRef voltage_states_key_per_cluster[CPUFREQ_NUM_CLUSTER_TYPES] = {CFSTR("voltage-states1"), CFSTR("voltage-states5")};

io_registry_entry_t pmgr_registry_entry = IORegistryEntryFromPath(kIOMainPortDefault, "IODeviceTree:/arm-io/pmgr");
if (pmgr_registry_entry == MACH_PORT_NULL) {
return -1;
}
for (size_t i = 0U; i < CPUFREQ_NUM_CLUSTER_TYPES; i++) {
CFDataRef voltage_states_ref =
(CFDataRef) IORegistryEntryCreateCFProperty(pmgr_registry_entry, voltage_states_key_per_cluster[i], kCFAllocatorDefault, 0U);
if (voltage_states_ref == NULL) {
IOObjectRelease(pmgr_registry_entry);
return -1;
}

CpuFreqPowerStateFrequencies* cluster_frequencies = &data->cpu_frequencies_per_cluster_type[i];
cluster_frequencies->num_frequencies = CFDataGetLength(voltage_states_ref) / 8;
cluster_frequencies->frequencies = xCalloc(cluster_frequencies->num_frequencies, sizeof(double));
const uint8_t* voltage_states_data = CFDataGetBytePtr(voltage_states_ref);
for (size_t j = 0U; j < cluster_frequencies->num_frequencies; j++) {
uint32_t freq_value;
memcpy(&freq_value, voltage_states_data + j * 8, 4);
cluster_frequencies->frequencies[j] = (65536000.0 / freq_value) * 1000000;
}
CFRelease(voltage_states_ref);
}
IOObjectRelease(pmgr_registry_entry);


/* Create subscription for CPU performance states */
CFMutableDictionaryRef channels = IOReportCopyChannelsInGroup(CFSTR("CPU Stats"), CFSTR("CPU Core Performance States"), NULL, NULL);
if (channels == NULL) {
return -1;
}

data->subscribed_channels = NULL;
data->subscription = IOReportCreateSubscription(NULL, channels, &data->subscribed_channels, 0U, NULL);

CFRelease(channels);

if (data->subscription == NULL) {
return -1;
}

data->prev_samples = NULL;

return 0;
}

void CpuFreq_update(CpuFreqData* data) {
CFDictionaryRef samples = IOReportCreateSamples(data->subscription, data->subscribed_channels, NULL);
if (samples == NULL) {
return;
}

if (data->prev_samples == NULL) {
data->prev_samples = samples;
return;
}

/* Residency is cumulative, we have to diff two samples to get a current view */
CFDictionaryRef samples_delta = IOReportCreateSamplesDelta(data->prev_samples, samples, NULL);

/* Iterate through performance state residencies. Index 0 is the idle residency, index 1-n is the per-performance state residency. */
__block uint32_t cpu_i = 0U;
IOReportIterate(samples_delta, ^(IOReportSampleRef ch) {
if (cpu_i >= data->existingCPUs) {
/* The report contains more CPUs than we know about. This should not happen. */
return kIOReportIterOk; // TODO: find way to possibly stop iteration early on error
}
const CpuFreqPowerStateFrequencies* cpu_frequencies = &data->cpu_frequencies_per_cluster_type[data->cluster_type_per_cpu[cpu_i]];
uint32_t state_count = IOReportStateGetCount(ch);
if (state_count != cpu_frequencies->num_frequencies + 1) {
/* The number of reported states does not match the number of previously queried frequencies. This should not happen. */
return kIOReportIterOk; // TODO: find way to possibly stop iteration early on error
}
/* Calculate average frequency based on residency and per-performance state frequency */
double average_freq = 0.0;
int64_t total_residency = 0U;
for (uint32_t i = 0U; i < state_count; i++) {
const int64_t residency = IOReportStateGetResidency(ch, i);
total_residency += residency;
/* We count idle as the smallest frequency */
average_freq += residency * cpu_frequencies->frequencies[(i == 0) ? 0 : (i - 1)];
}
average_freq /= total_residency;
data->frequencies[cpu_i] = average_freq / 1000000; // Convert to MHz

cpu_i++;
return kIOReportIterOk;
});
CFRelease(samples_delta);

CFRelease(data->prev_samples);
data->prev_samples = samples;
}

void CpuFreq_cleanup(CpuFreqData* data) {
if (data->subscription != NULL) {
CFRelease(data->subscription);
}
if (data->subscribed_channels != NULL) {
CFRelease(data->subscribed_channels);
}
if (data->prev_samples != NULL) {
CFRelease(data->prev_samples);
}
free(data->cluster_type_per_cpu);
free(data->frequencies);
for (size_t i = 0U; i < CPUFREQ_NUM_CLUSTER_TYPES; i++) {
free(data->cpu_frequencies_per_cluster_type[i].frequencies);
}
}
#endif
66 changes: 66 additions & 0 deletions darwin/CpuFreq.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#ifndef HEADER_CpuFreq
#define HEADER_CpuFreq

#include "Machine.h"

#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);

/* Definitions */
typedef struct {
uint32_t num_frequencies;
double* frequencies;
} CpuFreqPowerStateFrequencies;

/*
* Seems to be hardcoded for now on all Apple Silicon platforms, no way to get it dynamically.
* Current cluster types are "E" for efficiency cores and "P" for performance cores.
*/
#define CPUFREQ_NUM_CLUSTER_TYPES 2

typedef struct {
/* Number of CPUs */
unsigned int existingCPUs;

/* existingCPUs records, containing which CPU belongs to which cluster type ("E": 0, "P": 1) */
uint32_t* cluster_type_per_cpu;

/* Frequencies for all power states per cluster type */
CpuFreqPowerStateFrequencies cpu_frequencies_per_cluster_type[CPUFREQ_NUM_CLUSTER_TYPES];

/* IOReport subscription handlers */
IOReportSubscriptionRef subscription;
CFMutableDictionaryRef subscribed_channels;

/* Last IOReport sample */
CFDictionaryRef prev_samples;

/* existingCPUs records, containing last determined frequency per CPU in MHz */
double* frequencies;
} CpuFreqData;

int CpuFreq_init(const Machine* machine, CpuFreqData* data);
void CpuFreq_update(CpuFreqData* data);
void CpuFreq_cleanup(CpuFreqData* data);
#endif

#endif
12 changes: 12 additions & 0 deletions darwin/DarwinMachine.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
#endif
}

Machine* Machine_new(UsersTable* usersTable, uid_t userId) {
Expand All @@ -97,6 +100,11 @@ Machine* Machine_new(UsersTable* usersTable, uid_t userId) {
openzfs_sysctl_init(&this->zfs);
openzfs_sysctl_updateArcStats(&this->zfs);

#ifdef CPUFREQ_SUPPORT
/* Initialize CPU frequency data */
this->cpu_freq_ok = CpuFreq_init(&this->super, &this->cpu_freq) == 0;
#endif

return super;
}

Expand All @@ -105,6 +113,10 @@ void Machine_delete(Machine* super) {

DarwinMachine_freeCPULoadInfo(&this->prev_load);

#ifdef CPUFREQ_SUPPORT
CpuFreq_cleanup(&this->cpu_freq);
#endif

Machine_done(super);
free(this);
}
Expand Down
6 changes: 5 additions & 1 deletion darwin/DarwinMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ in the source distribution for its full text.

#include "Machine.h"
#include "zfs/ZfsArcStats.h"

#include "CpuFreq.h"

typedef struct DarwinMachine_ {
Machine super;
Expand All @@ -21,6 +21,10 @@ typedef struct DarwinMachine_ {
vm_statistics_data_t vm_stats;
processor_cpu_load_info_t prev_load;
processor_cpu_load_info_t curr_load;
#ifdef CPUFREQ_SUPPORT
CpuFreqData cpu_freq;
bool cpu_freq_ok;
#endif

ZfsArcStats zfs;
} DarwinMachine;
Expand Down
8 changes: 8 additions & 0 deletions darwin/Platform.c
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,15 @@ double Platform_setCPUValues(Meter* mtr, unsigned int cpu) {
/* Convert to percent and return */
total = mtr->values[CPU_METER_NICE] + mtr->values[CPU_METER_NORMAL] + mtr->values[CPU_METER_KERNEL];

#ifdef CPUFREQ_SUPPORT
if (dhost->cpu_freq_ok) {
mtr->values[CPU_METER_FREQUENCY] = dhost->cpu_freq.frequencies[cpu - 1];
} else {
mtr->values[CPU_METER_FREQUENCY] = NAN;
}
#else
mtr->values[CPU_METER_FREQUENCY] = NAN;
#endif
mtr->values[CPU_METER_TEMPERATURE] = NAN;

return CLAMP(total, 0.0, 100.0);
Expand Down

0 comments on commit cba2a28

Please sign in to comment.