CPU Core Temperatures & Thermal Zone Monitoring #3

Closed
opened 2026-09-08 10:18:25 +01:00 by gronod · 2 comments
Owner

Purpose

Monitor real-time temperatures for Intel CPU dies and individual CPU cores, identifying thermal hotspots, thermal margin, and active hardware thermal throttling on Intel Macs.

Priority

High (Primary metric for Intel Mac performance degradation and cooling health)

Dependencies

  • Depends on #2 (SMC Interface Engine)

Scope

  • Query SMC temperature keys for CPU Package (TC0P, TC0D, TCXC), CPU Cores (TC0C, TC1C, TC2C... up to the available core count), and Intel PECI (Platform Environment Control Interface) readings.
  • Dynamically detect the number of active CPU cores and map corresponding SMC keys.
  • Detect thermal throttling conditions (PROCHOT / frequency capping / thermal emergency states).
  • Present real-time temperature gauges, peak/average temperatures, and thermal pressure status in SwiftUI.

Implementation Suggestions

  • Query known Intel SMC key patterns such as TC[0-9]C (Core temperatures), TC0F (Die temperature), and TC0P (Proximity).
  • Cross-reference core counts using sysctlbyname("hw.ncpu", ...) or sysctlbyname("hw.physicalcpu", ...).
  • Query thermal pressure level notifications via OSThermalNotification or ProcessInfo.processInfo.thermalState to detect OS-level throttling intervention.
  • Provide temperature units toggle (°C / °F) in the presentation layer.
### Purpose Monitor real-time temperatures for Intel CPU dies and individual CPU cores, identifying thermal hotspots, thermal margin, and active hardware thermal throttling on Intel Macs. ### Priority **High** (Primary metric for Intel Mac performance degradation and cooling health) ### Dependencies - Depends on #2 (SMC Interface Engine) ### Scope - Query SMC temperature keys for CPU Package (`TC0P`, `TC0D`, `TCXC`), CPU Cores (`TC0C`, `TC1C`, `TC2C`... up to the available core count), and Intel PECI (Platform Environment Control Interface) readings. - Dynamically detect the number of active CPU cores and map corresponding SMC keys. - Detect thermal throttling conditions (PROCHOT / frequency capping / thermal emergency states). - Present real-time temperature gauges, peak/average temperatures, and thermal pressure status in SwiftUI. ### Implementation Suggestions - Query known Intel SMC key patterns such as `TC[0-9]C` (Core temperatures), `TC0F` (Die temperature), and `TC0P` (Proximity). - Cross-reference core counts using `sysctlbyname("hw.ncpu", ...)` or `sysctlbyname("hw.physicalcpu", ...)`. - Query thermal pressure level notifications via `OSThermalNotification` or `ProcessInfo.processInfo.thermalState` to detect OS-level throttling intervention. - Provide temperature units toggle (°C / °F) in the presentation layer.
gronod added the Kind/Feature
Priority
High
2
labels 2026-09-08 10:18:25 +01:00
gronod added the Project/AntigravityFeature/BackendFeature/UI labels 2026-09-08 10:28:21 +01:00
Author
Owner

Technical Implementation Plan & Code Solution

1. Intel Thermal Architecture & Key Matrix

Intel CPUs on macOS report internal DTS (Digital Thermal Sensor) core temperatures, PECI (Platform Environment Control Interface) readings, and thermal diode proximity values through AppleSMC.

flowchart LR
    DTS["Intel Core 0-N DTS"] -->|"SMC Keys: TC0C, TC1C, TC2C..."| PROV["MMCPUThermalProvider<br/>(sp78 decode)"]
    PKG["Intel Package / Die"] -->|"SMC Keys: TC0P, TC0D, TCXC"| PROV
    THROT["Thermal Throttling"] -->|"NSProcessInfo.thermalState"| UI["SwiftUI Gauge & Warnings"]
    PROV -->|"Normalized Temps (°C)"| UI

Key Catalog for Intel Architectures

  • CPU Package / Proximity: TC0P (Proximity), TC0D (Die), TC0E, TC0F
  • CPU Cores: TC0C through TC15C (dynamically matched to physical core count)
  • PECI (Platform Environment Control Interface): TCXC, TCSC

2. Objective-C Provider (MMCPUThermalProvider.m)

#import "MMCPUThermalProvider.h"
#import "MMAppleSMCClient.h"
#import <sys/sysctl.h>

@interface MMCPUThermalProvider ()
@property (nonatomic, assign) NSInteger physicalCoreCount;
@property (nonatomic, strong) NSArray<NSString *> *discoveredCoreKeys;
@property (nonatomic, strong) NSString *packageKey;
@end

@implementation MMCPUThermalProvider

- (instancetype)init {
    self = [super init];
    if (self) {
        _physicalCoreCount = [self queryPhysicalCoreCount];
        [self discoverThermalKeys];
    }
    return self;
}

- (NSInteger)queryPhysicalCoreCount {
    int count = 0;
    size_t size = sizeof(count);
    if (sysctlbyname("hw.physicalcpu", &count, &size, NULL, 0) == 0 && count > 0) {
        return count;
    }
    return 4; // Fallback
}

- (void)discoverThermalKeys {
    MMAppleSMCClient *smc = [MMAppleSMCClient sharedClient];
    NSMutableArray *cores = [NSMutableArray array];

    // Detect CPU package key
    NSArray *candidatePackageKeys = @[@"TC0P", @"TC0D", @"TC0F", @"TCXC"];
    for (NSString *key in candidatePackageKeys) {
        if ([smc readTemperatureForKey:key] != nil) {
            self.packageKey = key;
            break;
        }
    }

    // Detect per-core keys (TC0C, TC1C...)
    for (NSInteger i = 0; i < self.physicalCoreCount; i++) {
        NSString *key = [NSString stringWithFormat:@"TC%ldC", (long)i];
        if ([smc readTemperatureForKey:key] != nil) {
            [cores addObject:key];
        }
    }
    self.discoveredCoreKeys = [cores copy];
}

- (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
    MMAppleSMCClient *smc = [MMAppleSMCClient sharedClient];
    NSMutableDictionary *sample = [NSMutableDictionary dictionary];

    // 1. Package Temp
    if (self.packageKey) {
        NSNumber *pkg = [smc readTemperatureForKey:self.packageKey];
        if (pkg) sample[@"packageTemperature"] = pkg;
    }

    // 2. Per-Core Temps
    NSMutableArray *coreTemps = [NSMutableArray arrayWithCapacity:self.discoveredCoreKeys.count];
    double sum = 0;
    for (NSString *key in self.discoveredCoreKeys) {
        NSNumber *temp = [smc readTemperatureForKey:key];
        if (temp) {
            [coreTemps addObject:@{@"key": key, @"temperature": temp}];
            sum += temp.doubleValue;
        }
    }
    sample[@"cores"] = coreTemps;
    if (coreTemps.count > 0) {
        sample[@"averageCoreTemperature"] = @(sum / coreTemps.count);
    }

    // 3. Thermal State & Throttling
    NSProcessInfoThermalState state = [NSProcessInfo processInfo].thermalState;
    sample[@"thermalState"] = @((NSInteger)state);
    sample[@"isThrottling"] = @(state >= NSProcessInfoThermalStateSerious);

    return [sample copy];
}
@end

3. Swift ViewModel & Contract

public struct CoreTemperature: Identifiable, Sendable {
    public var id: String { key }
    public let key: String
    public let temperatureCelsius: Double
}

@Observable
public final class ThermalModel {
    public var packageTemperature: Double = 0.0
    public var coreTemperatures: [CoreTemperature] = []
    public var isThrottled: Bool = false
    public var thermalPressureState: ProcessInfo.ThermalState = .nominal

    public func update(with dict: [String: Any]) {
        if let pkg = dict["packageTemperature"] as? Double {
            self.packageTemperature = pkg
        }
        if let cores = dict["cores"] as? [[String: Any]] {
            self.coreTemperatures = cores.compactMap { item in
                guard let key = item["key"] as? String,
                      let temp = item["temperature"] as? Double else { return nil }
                return CoreTemperature(key: key, temperatureCelsius: temp)
            }
        }
        if let throttled = dict["isThrottling"] as? Bool {
            self.isThrottled = throttled
        }
    }
}

4. Edge Cases Handled

  • Missing Core Keys: Dynamically discovers available keys during initialization so models with fewer exposed core sensors (e.g. older MacBook Airs vs 18-core iMac Pro) don't query invalid keys.
  • Sensor Glitches: The SMC parser ignores outliers (< 0°C or > 130°C) to eliminate spurious sensor read errors.
## Technical Implementation Plan & Code Solution ### 1. Intel Thermal Architecture & Key Matrix Intel CPUs on macOS report internal DTS (Digital Thermal Sensor) core temperatures, PECI (Platform Environment Control Interface) readings, and thermal diode proximity values through AppleSMC. ```mermaid flowchart LR DTS["Intel Core 0-N DTS"] -->|"SMC Keys: TC0C, TC1C, TC2C..."| PROV["MMCPUThermalProvider<br/>(sp78 decode)"] PKG["Intel Package / Die"] -->|"SMC Keys: TC0P, TC0D, TCXC"| PROV THROT["Thermal Throttling"] -->|"NSProcessInfo.thermalState"| UI["SwiftUI Gauge & Warnings"] PROV -->|"Normalized Temps (°C)"| UI ``` #### Key Catalog for Intel Architectures - **CPU Package / Proximity**: `TC0P` (Proximity), `TC0D` (Die), `TC0E`, `TC0F` - **CPU Cores**: `TC0C` through `TC15C` (dynamically matched to physical core count) - **PECI (Platform Environment Control Interface)**: `TCXC`, `TCSC` --- ### 2. Objective-C Provider (`MMCPUThermalProvider.m`) ```objc #import "MMCPUThermalProvider.h" #import "MMAppleSMCClient.h" #import <sys/sysctl.h> @interface MMCPUThermalProvider () @property (nonatomic, assign) NSInteger physicalCoreCount; @property (nonatomic, strong) NSArray<NSString *> *discoveredCoreKeys; @property (nonatomic, strong) NSString *packageKey; @end @implementation MMCPUThermalProvider - (instancetype)init { self = [super init]; if (self) { _physicalCoreCount = [self queryPhysicalCoreCount]; [self discoverThermalKeys]; } return self; } - (NSInteger)queryPhysicalCoreCount { int count = 0; size_t size = sizeof(count); if (sysctlbyname("hw.physicalcpu", &count, &size, NULL, 0) == 0 && count > 0) { return count; } return 4; // Fallback } - (void)discoverThermalKeys { MMAppleSMCClient *smc = [MMAppleSMCClient sharedClient]; NSMutableArray *cores = [NSMutableArray array]; // Detect CPU package key NSArray *candidatePackageKeys = @[@"TC0P", @"TC0D", @"TC0F", @"TCXC"]; for (NSString *key in candidatePackageKeys) { if ([smc readTemperatureForKey:key] != nil) { self.packageKey = key; break; } } // Detect per-core keys (TC0C, TC1C...) for (NSInteger i = 0; i < self.physicalCoreCount; i++) { NSString *key = [NSString stringWithFormat:@"TC%ldC", (long)i]; if ([smc readTemperatureForKey:key] != nil) { [cores addObject:key]; } } self.discoveredCoreKeys = [cores copy]; } - (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error { MMAppleSMCClient *smc = [MMAppleSMCClient sharedClient]; NSMutableDictionary *sample = [NSMutableDictionary dictionary]; // 1. Package Temp if (self.packageKey) { NSNumber *pkg = [smc readTemperatureForKey:self.packageKey]; if (pkg) sample[@"packageTemperature"] = pkg; } // 2. Per-Core Temps NSMutableArray *coreTemps = [NSMutableArray arrayWithCapacity:self.discoveredCoreKeys.count]; double sum = 0; for (NSString *key in self.discoveredCoreKeys) { NSNumber *temp = [smc readTemperatureForKey:key]; if (temp) { [coreTemps addObject:@{@"key": key, @"temperature": temp}]; sum += temp.doubleValue; } } sample[@"cores"] = coreTemps; if (coreTemps.count > 0) { sample[@"averageCoreTemperature"] = @(sum / coreTemps.count); } // 3. Thermal State & Throttling NSProcessInfoThermalState state = [NSProcessInfo processInfo].thermalState; sample[@"thermalState"] = @((NSInteger)state); sample[@"isThrottling"] = @(state >= NSProcessInfoThermalStateSerious); return [sample copy]; } @end ``` --- ### 3. Swift ViewModel & Contract ```swift public struct CoreTemperature: Identifiable, Sendable { public var id: String { key } public let key: String public let temperatureCelsius: Double } @Observable public final class ThermalModel { public var packageTemperature: Double = 0.0 public var coreTemperatures: [CoreTemperature] = [] public var isThrottled: Bool = false public var thermalPressureState: ProcessInfo.ThermalState = .nominal public func update(with dict: [String: Any]) { if let pkg = dict["packageTemperature"] as? Double { self.packageTemperature = pkg } if let cores = dict["cores"] as? [[String: Any]] { self.coreTemperatures = cores.compactMap { item in guard let key = item["key"] as? String, let temp = item["temperature"] as? Double else { return nil } return CoreTemperature(key: key, temperatureCelsius: temp) } } if let throttled = dict["isThrottling"] as? Bool { self.isThrottled = throttled } } } ``` --- ### 4. Edge Cases Handled - **Missing Core Keys**: Dynamically discovers available keys during initialization so models with fewer exposed core sensors (e.g. older MacBook Airs vs 18-core iMac Pro) don't query invalid keys. - **Sensor Glitches**: The SMC parser ignores outliers (`< 0°C` or `> 130°C`) to eliminate spurious sensor read errors.
gronod added this to the M2: Primary System & Thermal Telemetry milestone 2026-09-08 10:41:58 +01:00
gronod added a new dependency 2026-09-08 11:37:03 +01:00
gronod added a new dependency 2026-09-08 11:37:11 +01:00
Author
Owner

Completed in feat/3-cpu-thermals and merged into develop.

  • Implemented MMCPUThermalProvider probing AppleSMC package temperature keys (TC0P, TC0D, TC0H, TCXC).
  • Enumerated per-core temperature keys (TC0C .. TCnC) based on physical core counts.
  • Queried OS thermal pressure states via NSProcessInfoThermalState.
  • Implemented calculations for peak core temperature, average core temperature, and throttling flags.
  • Unit tested with 100% pass in MMCPUThermalTests.swift.
Completed in `feat/3-cpu-thermals` and merged into `develop`. - Implemented `MMCPUThermalProvider` probing AppleSMC package temperature keys (`TC0P`, `TC0D`, `TC0H`, `TCXC`). - Enumerated per-core temperature keys (`TC0C` .. `TCnC`) based on physical core counts. - Queried OS thermal pressure states via `NSProcessInfoThermalState`. - Implemented calculations for peak core temperature, average core temperature, and throttling flags. - Unit tested with 100% pass in `MMCPUThermalTests.swift`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#3