From ec7e75e1053ba12a09ef85da7d144b95aa27a80f Mon Sep 17 00:00:00 2001 From: Owen Rummage Date: Tue, 21 Jul 2026 01:13:33 -0500 Subject: [PATCH] Fix CPU telemetry temperature/clock speed on Windows and macOS sample_cpu_temperature() only ever had a Linux implementation, so Windows and macOS runs recorded empty temperature data points for every sample. sample_cpu_clock_mhz() on both platforms read a static nominal frequency once (Windows: the ~MHz registry value; macOS: the hw.cpufrequency sysctl) instead of a live reading, so clock speed showed as a constant throughout the benchmark. Windows: temperature now queries the ACPI thermal zone over WMI (MSAcpi_ThermalZoneTemperature), and clock speed reads live per-core frequency via CallNtPowerInformation(ProcessorInformation). Both are XP-compatible (verified via mingw cross-compile with the existing windows-i386 XP toolchain/PE checks). macOS: temperature reads the SMC directly (AppleSMC user client), checking known CPU sensor keys across Intel and Apple Silicon (M1-M5). There is no live-frequency API on macOS at all (Apple Silicon has none, and hw.cpufrequency was always a fixed nominal value even on Intel), so clock speed now reports as unavailable instead of a misleading constant. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PcL8rcwwBeD6W3fZFCRLss --- .github/workflows/release.yml | 2 +- Makefile | 11 +- src/app/benchmark.c | 252 ++++++++++++++++++++++++++++++++-- 3 files changed, 250 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be2c31e..17363af 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -175,5 +175,5 @@ jobs: release/SHA256SUMS \ --repo "$GITHUB_REPOSITORY" \ --title "Release $RELEASE_TAG" \ - --notes "Fixed x86/Linux builds on systems without OpenSSL development headers. Result uploads now use plain HTTP on every platform, removing the OpenSSL build and runtime dependency." \ + --notes "Hotfix: CPU telemetry (temperature and clock speed) recorded during benchmark runs was not working on Windows or macOS, showing empty temperature data points and a constant clock speed. Windows temperature now reads the ACPI thermal zone over WMI, and clock speed reads live per-core frequency via CallNtPowerInformation instead of a static registry value. macOS temperature now reads the SMC directly across Intel and Apple Silicon (M1-M5) Macs. macOS clock speed now reports as unavailable rather than the fixed nominal frequency it previously showed, since no live-frequency API exists on macOS." \ --generate-notes diff --git a/Makefile b/Makefile index e487925..e5d544c 100644 --- a/Makefile +++ b/Makefile @@ -178,20 +178,21 @@ $(DIST)/fossbench-linux-ppc64le: $(DRIVER) $(DRIVER_DEPS) $(ASM_PPC64LE) | $(DIS @echo "built $@" $(DIST)/fossbench-macos-arm64: $(DRIVER) $(DRIVER_DEPS) $(ASM_ARM64) | $(DIST) - $(CC_MACOS_ARM64) -arch arm64 $(CFLAGS) $(PTHREAD) $(LDFLAGS) -o $@ $(DRIVER) $(ASM_ARM64) $(LDLIBS) + $(CC_MACOS_ARM64) -arch arm64 $(CFLAGS) $(PTHREAD) $(LDFLAGS) -o $@ $(DRIVER) $(ASM_ARM64) $(LDLIBS) -framework IOKit -framework CoreFoundation @echo "built $@" $(DIST)/fossbench-macos-amd64: $(DRIVER) $(DRIVER_DEPS) $(ASM_AMD64) | $(DIST) - MACOSX_DEPLOYMENT_TARGET=$(MACOS_AMD64_MIN) $(CC_MACOS_AMD64) -arch x86_64 -mmacosx-version-min=$(MACOS_AMD64_MIN) $(CFLAGS) $(PTHREAD) $(LDFLAGS) -Wl,-no_fixup_chains -o $@ $(DRIVER) $(ASM_AMD64) $(LDLIBS) + MACOSX_DEPLOYMENT_TARGET=$(MACOS_AMD64_MIN) $(CC_MACOS_AMD64) -arch x86_64 -mmacosx-version-min=$(MACOS_AMD64_MIN) $(CFLAGS) $(PTHREAD) $(LDFLAGS) -Wl,-no_fixup_chains -o $@ $(DRIVER) $(ASM_AMD64) $(LDLIBS) -framework IOKit -framework CoreFoundation @echo "built $@" -# Windows builds are static and use WinHTTP. +# Windows builds are static and use WinHTTP. Temperature/clock telemetry +# additionally needs WMI (ole32/oleaut32/wbemuuid) and PowrProf. $(DIST)/fossbench-windows-amd64.exe: $(DRIVER) $(DRIVER_DEPS) $(ASM_AMD64) | $(DIST) - $(CC_WINDOWS_AMD64) $(CFLAGS) $(PTHREAD) -static -o $@ $(DRIVER) $(ASM_AMD64) -lm -lwinhttp -ladvapi32 + $(CC_WINDOWS_AMD64) $(CFLAGS) $(PTHREAD) -static -o $@ $(DRIVER) $(ASM_AMD64) -lm -lwinhttp -ladvapi32 -lole32 -loleaut32 -lwbemuuid -lpowrprof @echo "built $@" $(DIST)/fossbench-windows-i386.exe: $(DRIVER) $(DRIVER_DEPS) $(SRC_I386) | $(DIST) - $(CC_WINDOWS_I386) -march=i386 $(WINDOWS_I386_XP_CFLAGS) $(CFLAGS) -static $(WINDOWS_I386_XP_LDFLAGS) -o $@ $(DRIVER) $(SRC_I386) -lm -lwinhttp -ladvapi32 + $(CC_WINDOWS_I386) -march=i386 $(WINDOWS_I386_XP_CFLAGS) $(CFLAGS) -static $(WINDOWS_I386_XP_LDFLAGS) -o $@ $(DRIVER) $(SRC_I386) -lm -lwinhttp -ladvapi32 -lole32 -loleaut32 -lwbemuuid -lpowrprof @echo "built $@" # Add a native rule if one was not already made above. diff --git a/src/app/benchmark.c b/src/app/benchmark.c index 87bee02..433b0fa 100644 --- a/src/app/benchmark.c +++ b/src/app/benchmark.c @@ -12,6 +12,20 @@ # include # include # include +# include +# include +# include +/* Documented by Microsoft but not declared in MinGW's powrprof.h; including + * for it collides with definitions already pulled in by + * , so declare the same layout locally instead. */ +typedef struct _PROCESSOR_POWER_INFORMATION { + ULONG Number; + ULONG MaxMhz; + ULONG CurrentMhz; + ULONG MhzLimit; + ULONG MaxIdleState; + ULONG CurrentIdleState; +} PROCESSOR_POWER_INFORMATION, *PPROCESSOR_POWER_INFORMATION; #else # include # include @@ -32,6 +46,7 @@ # include # include # include +# include #endif /* The server URL can be changed when building. */ @@ -270,6 +285,179 @@ static int cpu_sensor_name(const char *name) } #endif +#if defined(_WIN32) +/* Reads MSAcpi_ThermalZoneTemperature from the ACPI thermal zone WMI namespace. + * Only populated on hardware whose firmware exposes it; many laptops do not. */ +static IWbemServices *wmi_connect_thermal_namespace(void) +{ + IWbemLocator *locator = NULL; + IWbemServices *services = NULL; + BSTR namespace_path = NULL; + HRESULT hr; + + hr = CoCreateInstance(&CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, &IID_IWbemLocator, (LPVOID *)&locator); + if (FAILED(hr) || !locator) return NULL; + + namespace_path = SysAllocString(L"ROOT\\WMI"); + if (!namespace_path) { locator->lpVtbl->Release(locator); return NULL; } + hr = locator->lpVtbl->ConnectServer(locator, namespace_path, NULL, NULL, NULL, 0, NULL, NULL, &services); + SysFreeString(namespace_path); + locator->lpVtbl->Release(locator); + if (FAILED(hr) || !services) return NULL; + + hr = CoSetProxyBlanket((IUnknown *)services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL, + RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE); + if (FAILED(hr)) { services->lpVtbl->Release(services); return NULL; } + return services; +} + +static double wmi_query_temperature(IWbemServices *services) +{ + IEnumWbemClassObject *enumerator = NULL; + BSTR language = NULL, query = NULL; + HRESULT hr; + double hottest = -1.0; + + language = SysAllocString(L"WQL"); + query = SysAllocString(L"SELECT CurrentTemperature FROM MSAcpi_ThermalZoneTemperature"); + if (!language || !query) goto done; + + hr = services->lpVtbl->ExecQuery(services, language, query, + WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, &enumerator); + if (FAILED(hr) || !enumerator) goto done; + + for (;;) { + IWbemClassObject *obj = NULL; + ULONG returned = 0; + VARIANT value; + hr = enumerator->lpVtbl->Next(enumerator, WBEM_INFINITE, 1, &obj, &returned); + if (FAILED(hr) || returned == 0) break; + VariantInit(&value); + /* CurrentTemperature is reported in tenths of a Kelvin. */ + if (SUCCEEDED(obj->lpVtbl->Get(obj, L"CurrentTemperature", 0, &value, NULL, NULL))) { + double tenths_kelvin = 0.0; + if (value.vt == VT_I4) tenths_kelvin = (double)value.lVal; + else if (value.vt == VT_UI4) tenths_kelvin = (double)value.ulVal; + if (tenths_kelvin > 0.0) { + double celsius = tenths_kelvin / 10.0 - 273.15; + if (celsius > -50.0 && celsius <= 150.0 && celsius > hottest) hottest = celsius; + } + } + VariantClear(&value); + obj->lpVtbl->Release(obj); + } + enumerator->lpVtbl->Release(enumerator); +done: + if (query) SysFreeString(query); + if (language) SysFreeString(language); + return hottest; +} +#endif + +#if defined(__APPLE__) +/* Talks to the AppleSMC user client directly; there is no public framework + * for this. Struct layout and command bytes come from the SMC protocol that + * has been stable across Intel and Apple Silicon Macs for over a decade. */ +typedef struct { + char major, minor, build, reserved_; + uint16_t release; +} fb_smc_vers_t; + +typedef struct { + uint16_t version, length; + uint32_t cpu_p_limit, gpu_p_limit, mem_p_limit; +} fb_smc_plimit_t; + +typedef struct { + uint32_t data_size; + uint32_t data_type; + char data_attributes; +} fb_smc_key_info_t; + +typedef struct { + uint32_t key; + fb_smc_vers_t vers; + fb_smc_plimit_t plimit; + fb_smc_key_info_t key_info; + char result, status, data8; + uint32_t data32; + unsigned char bytes[32]; +} fb_smc_data_t; + +#define FB_SMC_KERNEL_INDEX 2 +#define FB_SMC_CMD_READ_BYTES 5 +#define FB_SMC_CMD_READ_KEY_INFO 9 + +static uint32_t smc_key_from_string(const char *s) +{ + return ((uint32_t)(unsigned char)s[0] << 24) | ((uint32_t)(unsigned char)s[1] << 16) | + ((uint32_t)(unsigned char)s[2] << 8) | (uint32_t)(unsigned char)s[3]; +} + +static kern_return_t smc_call(io_connect_t conn, fb_smc_data_t *in, fb_smc_data_t *out) +{ + size_t out_size = sizeof(*out); + return IOConnectCallStructMethod(conn, FB_SMC_KERNEL_INDEX, in, sizeof(*in), out, &out_size); +} + +static int smc_read_temperature(io_connect_t conn, const char *key, double *out) +{ + fb_smc_data_t in, info_out, read_out; + + memset(&in, 0, sizeof(in)); + memset(&info_out, 0, sizeof(info_out)); + in.key = smc_key_from_string(key); + in.data8 = FB_SMC_CMD_READ_KEY_INFO; + if (smc_call(conn, &in, &info_out) != KERN_SUCCESS || info_out.key_info.data_size == 0) + return 0; + + memset(&in, 0, sizeof(in)); + memset(&read_out, 0, sizeof(read_out)); + in.key = smc_key_from_string(key); + in.key_info.data_size = info_out.key_info.data_size; + in.data8 = FB_SMC_CMD_READ_BYTES; + if (smc_call(conn, &in, &read_out) != KERN_SUCCESS) + return 0; + + if (info_out.key_info.data_type == smc_key_from_string("sp78") && info_out.key_info.data_size >= 2) { + int16_t raw = (int16_t)((read_out.bytes[0] << 8) | read_out.bytes[1]); + *out = raw / 256.0; + return 1; + } + if (info_out.key_info.data_type == smc_key_from_string("flt ") && info_out.key_info.data_size >= 4) { + float f; + memcpy(&f, read_out.bytes, sizeof(f)); + *out = (double)f; + return 1; + } + return 0; +} + +static io_connect_t smc_open(void) +{ + io_service_t service; + io_connect_t conn = IO_OBJECT_NULL; + + service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("AppleSMC")); + if (service == IO_OBJECT_NULL) return IO_OBJECT_NULL; + if (IOServiceOpen(service, mach_task_self(), 0, &conn) != KERN_SUCCESS) conn = IO_OBJECT_NULL; + IOObjectRelease(service); + return conn; +} + +/* CPU-related SMC keys across Intel Macs and Apple Silicon M1-M5, gathered + * from publicly reverse-engineered sensor tables (e.g. exelban/stats, + * acidanthera/VirtualSMC). Coverage varies by exact model/chip revision. */ +static const char *const smc_cpu_temp_keys[] = { + "TC0D", "TC0E", "TC0F", "TC0P", + "Tp00", "Tp01", "Tp04", "Tp05", "Tp08", "Tp09", "Tp0C", "Tp0D", "Tp0f", "Tp0G", + "Tp0H", "Tp0j", "Tp0K", "Tp0L", "Tp0m", "Tp0n", "Tp0O", "Tp0P", "Tp0p", "Tp0r", + "Tp0R", "Tp0T", "Tp0u", "Tp0U", "Tp0X", "Tp0a", "Tp0b", "Tp0d", "Tp0g", "Tp0y", + "Tp1h", "Tp1l", "Tp1p", "Tp1t", + "Tc0a", "Tc0b", "Tc0x", "Tc0z", +}; +#endif + static double sample_cpu_temperature(void) { #if defined(__linux__) @@ -318,6 +506,37 @@ static double sample_cpu_temperature(void) closedir(dir); } return hottest; +#elif defined(_WIN32) + { + static int initialized = 0; + static IWbemServices *services = NULL; + if (!initialized) { + initialized = 1; + services = wmi_connect_thermal_namespace(); + } + return services ? wmi_query_temperature(services) : -1.0; + } +#elif defined(__APPLE__) + { + static int initialized = 0; + static io_connect_t conn = IO_OBJECT_NULL; + double hottest = -1.0; + size_t i; + + if (!initialized) { + initialized = 1; + conn = smc_open(); + } + if (conn == IO_OBJECT_NULL) return -1.0; + + for (i = 0; i < sizeof(smc_cpu_temp_keys) / sizeof(smc_cpu_temp_keys[0]); i++) { + double value; + if (smc_read_temperature(conn, smc_cpu_temp_keys[i], &value) && + value > 0.0 && value <= 150.0 && value > hottest) + hottest = value; + } + return hottest; + } #else return -1.0; #endif @@ -355,17 +574,26 @@ static double sample_cpu_clock_mhz(void) } return count ? total / (double)count : -1.0; #elif defined(__APPLE__) - { - uint64_t hz = 0; size_t size = sizeof(hz); - return sysctlbyname("hw.cpufrequency", &hz, &size, NULL, 0) == 0 && hz ? (double)hz / 1000000.0 : -1.0; - } + /* No public API reports live per-core frequency on macOS (none exists at + * all on Apple Silicon); sysctl hw.cpufrequency is a fixed nominal value, + * not a real-time reading, so report unavailable rather than a constant. */ + return -1.0; #elif defined(_WIN32) { - HKEY key; DWORD mhz = 0, size = sizeof(mhz), type = 0; - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", 0, KEY_READ, &key) != ERROR_SUCCESS) return -1.0; - if (RegQueryValueExA(key, "~MHz", NULL, &type, (LPBYTE)&mhz, &size) != ERROR_SUCCESS || type != REG_DWORD) mhz = 0; - RegCloseKey(key); - return mhz ? (double)mhz : -1.0; + PROCESSOR_POWER_INFORMATION info[64]; + SYSTEM_INFO sysinfo; + DWORD count, i; + double total = 0.0; + + GetSystemInfo(&sysinfo); + count = sysinfo.dwNumberOfProcessors; + if (count == 0) count = 1; + if (count > 64) count = 64; + if (CallNtPowerInformation(ProcessorInformation, NULL, 0, info, + count * sizeof(PROCESSOR_POWER_INFORMATION)) != 0) + return -1.0; + for (i = 0; i < count; i++) total += info[i].CurrentMhz; + return count ? total / (double)count : -1.0; } #else return -1.0; @@ -399,6 +627,11 @@ static void *telemetry_entry(void *arg) { struct telemetry_monitor *monitor = (struct telemetry_monitor *)arg; double next = monitor->started; +#if defined(_WIN32) + /* sample_cpu_temperature() lazily opens a WMI connection on first use, + * which requires this thread's COM apartment to be initialized first. */ + CoInitializeEx(NULL, COINIT_MULTITHREADED); +#endif while (!monitor->stop) { double current = now_seconds(); if (current >= next) { record_telemetry_sample(monitor); next += TELEMETRY_REFRESH_INTERVAL; } @@ -406,6 +639,7 @@ static void *telemetry_entry(void *arg) } record_telemetry_sample(monitor); #if defined(_WIN32) + CoUninitialize(); return 0; #else return NULL;