diff --git a/Makefile b/Makefile index 970e81882..4d5189a40 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ else ifeq ($(PLATFORM_LC),freebsd) else ifeq ($(PLATFORM_LC),macos) PLATFORM_DIR := osx THREADS := $(shell sysctl -n hw.ncpu || echo 1) - override ADDFLAGS += -framework IOKit -framework CoreFoundation -Wno-format-truncation + override ADDFLAGS += -framework IOKit -framework CoreFoundation -lIOReport -Wno-format-truncation SU_GROUP := wheel else $(error $(shell printf "\033[1;91mERROR: \033[97mUnsupported platform ($(PLATFORM))\033[0m")) @@ -205,6 +205,9 @@ SOURCES += $(sort $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEX SOURCE_COUNT := $(words $(SOURCES)) OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT))) +ifeq ($(PLATFORM_LC),macos) +OBJECTS += $(BUILDDIR)/osx/CpuFreq.o +endif ifeq ($(shell find $(BUILDDIR) -type f -newermt "$(DATESTAMP)" -name *.o >/dev/null 2>&1; echo $$?),0) ifneq ($(wildcard $(BUILDDIR)/.*),) @@ -378,5 +381,12 @@ $(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) | rocm_smi directories @$(CXX) $(CXXFLAGS) $(INC) -MMD -c -o $@ $< || exit 1 @printf "\033[1;92m$$($(PROGRESS))$(P)\033[10D\033[5C-> \033[1;37m$@ \033[100D\033[38C\033[1;93m(\033[1;97m$$(du -ah $@ | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$($(DATE_CMD) +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" + +ifeq ($(PLATFORM_LC),macos) +.ONESHELL: +$(BUILDDIR)/osx/CpuFreq.o: $(SRCDIR)/osx/CpuFreq.c + gcc -c -o $@ $(SRCDIR)/osx/CpuFreq.c +endif + #? Non-File Targets .PHONY: all msg help pre diff --git a/src/osx/CpuFreq.c b/src/osx/CpuFreq.c new file mode 100644 index 000000000..ffd8a3eea --- /dev/null +++ b/src/osx/CpuFreq.c @@ -0,0 +1,201 @@ +#include +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined (__aarch64__) +#include +#include +#include + +#include "CpuFreq.h" + +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000 +#define kIOMainPortDefault kIOMasterPortDefault +#endif + +/* Implementations */ +int CpuFreq_init(CpuFreqData *data) { + data->existingCPUs = 8; + data->cluster_type_per_cpu = (unsigned int *)malloc(data->existingCPUs * sizeof(uint32_t)); + data->frequencies = (double *)malloc(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 = + (double *)malloc(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 + } + double freq = 0.0; + double highest_residency = 0.0; + uint32_t highest_i = 0U; + for (uint32_t i = 0U; i < state_count; i++) { + const int64_t residency = IOReportStateGetResidency(ch, i); + if (residency > highest_residency) { + highest_i = i; + highest_residency = residency; + } + } + freq = cpu_frequencies->frequencies[highest_i == 0 ? 0 : highest_i - 1]; + data->frequencies[cpu_i] = round(freq); + + 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 diff --git a/src/osx/CpuFreq.h b/src/osx/CpuFreq.h new file mode 100644 index 000000000..1460cc36a --- /dev/null +++ b/src/osx/CpuFreq.h @@ -0,0 +1,77 @@ +#ifndef HEADER_CpuFreq +#define HEADER_CpuFreq + +#include +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) +#include +#include +#include + +/* Private API definitions from libIOReport*/ +enum { + kIOReportIterOk = 0, +}; +typedef struct IOReportSubscription *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(CpuFreqData *data); +void CpuFreq_update(CpuFreqData *data); +void CpuFreq_cleanup(CpuFreqData *data); +#endif +#endif diff --git a/src/osx/btop_collect.cpp b/src/osx/btop_collect.cpp index 681afc49a..3dd4d0440 100644 --- a/src/osx/btop_collect.cpp +++ b/src/osx/btop_collect.cpp @@ -50,14 +50,20 @@ tab-size = 4 #include #include #include -#include #include #include "../btop_config.hpp" #include "../btop_shared.hpp" #include "../btop_tools.hpp" -#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 +#if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000 +#define IOMainPort IOMasterPort +#endif + +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) +extern "C" { +#include "CpuFreq.h" +} #include "sensors.hpp" #endif #include "smc.hpp" @@ -191,6 +197,9 @@ namespace Cpu { bool has_battery = true; bool macM1 = false; tuple current_bat; +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) + CpuFreqData data; +#endif const array time_names = {"user", "nice", "system", "idle"}; @@ -253,13 +262,14 @@ namespace Cpu { Logger::debug("get_sensors(): show_coretemp=" + std::to_string(Config::getB("show_coretemp")) + " check_temp=" + std::to_string(Config::getB("check_temp"))); got_sensors = false; if (Config::getB("show_coretemp") and Config::getB("check_temp")) { -#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) ThermalSensors sensors; if (sensors.getSensors() > 0) { Logger::debug("M1 sensors found"); got_sensors = true; cpu_temp_only = true; macM1 = true; + CpuFreq_init(&data); } else { #endif // try SMC (intel) @@ -286,7 +296,7 @@ namespace Cpu { // ignore, we don't have temp got_sensors = false; } -#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) } #endif } @@ -297,7 +307,7 @@ namespace Cpu { current_cpu.temp_max = 95; // we have no idea how to get the critical temp try { if (macM1) { -#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) ThermalSensors sensors; current_cpu.temp.at(0).push_back(sensors.getSensors()); if (current_cpu.temp.at(0).size() > 20) @@ -324,15 +334,36 @@ namespace Cpu { } } +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) + unsigned int get_m1_cpuHz() { + int32_t freq = -1; + CpuFreq_update(&data); + double highest = 0; + for (unsigned int i = 0; i < data.existingCPUs; i++) { + if (data.frequencies[i] > highest) + highest = data.frequencies[i]; + } + return freq = (int32_t)highest; + } +#endif + string get_cpuHz() { - unsigned int freq = 1; + int32_t freq = 1; size_t size = sizeof(freq); int mib[] = {CTL_HW, HW_CPU_FREQ}; if (sysctl(mib, 2, &freq, &size, nullptr, 0) < 0) { // this fails on Apple Silicon macs. Apparently you're not allowed to know - return ""; +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) + freq = get_m1_cpuHz(); + if (freq < 0) { + // give up +#endif + return ""; +#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101504 && defined(__aarch64__) + } +#endif } return std::to_string(freq / 1000.0 / 1000.0 / 1000.0).substr(0, 3); } @@ -604,7 +635,7 @@ namespace Mem { io_iterator_t drive_list; mach_port_t libtop_master_port; - if (IOMasterPort(bootstrap_port, &libtop_master_port)) { + if (IOMainPort(bootstrap_port, &libtop_master_port)) { Logger::error("errot getting master port"); return; }