Host CPU Core Utilization, Frequency & Throttle Monitoring #5

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

Purpose

Deliver real-time telemetry on overall system CPU usage and individual per-core processor load (User, System, Nice, Idle), alongside clock frequency and CPU throttling indicators on Intel x86_64 architectures.

Priority

Critical (Fundamental system health and workload indicator)

Dependencies

  • Depends on #1 (Core Application Architecture & Objective-C/SwiftUI Bridging Foundation)

Scope

  • Query overall CPU utilization breakdown (User %, System %, Idle %, Nice %) via Mach kernel host statistics.
  • Query individual per-logical-core processor load via host_processor_info with PROCESSOR_CPU_LOAD_INFO.
  • Track CPU clock frequency (base clock vs current frequency via sysctl hw.cpufrequency or SMC frequency keys).
  • Detect CPU frequency throttling or thermal capping state.
  • Render multi-core activity bars/graphs in SwiftUI (e.g. 4, 8, 12, 16, 24, 28 core configurations on Mac Pro / iMac).

Implementation Suggestions

  • Call Mach API host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, ...) in Objective-C, calculating delta ticks between consecutive sampling intervals:
    delta_user = current.user - previous.user, etc.
  • Query sysctl variables machdep.cpu.brand_string, hw.physicalcpu, hw.logicalcpu, and hw.cpufrequency.
  • Expose an array of normalized per-core load values [Double] to SwiftUI for rendering dynamic core grid visualizations.
### Purpose Deliver real-time telemetry on overall system CPU usage and individual per-core processor load (User, System, Nice, Idle), alongside clock frequency and CPU throttling indicators on Intel x86_64 architectures. ### Priority **Critical** (Fundamental system health and workload indicator) ### Dependencies - Depends on #1 (Core Application Architecture & Objective-C/SwiftUI Bridging Foundation) ### Scope - Query overall CPU utilization breakdown (User %, System %, Idle %, Nice %) via Mach kernel host statistics. - Query individual per-logical-core processor load via `host_processor_info` with `PROCESSOR_CPU_LOAD_INFO`. - Track CPU clock frequency (base clock vs current frequency via sysctl `hw.cpufrequency` or SMC frequency keys). - Detect CPU frequency throttling or thermal capping state. - Render multi-core activity bars/graphs in SwiftUI (e.g. 4, 8, 12, 16, 24, 28 core configurations on Mac Pro / iMac). ### Implementation Suggestions - Call Mach API `host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, ...)` in Objective-C, calculating delta ticks between consecutive sampling intervals: `delta_user = current.user - previous.user`, etc. - Query sysctl variables `machdep.cpu.brand_string`, `hw.physicalcpu`, `hw.logicalcpu`, and `hw.cpufrequency`. - Expose an array of normalized per-core load values `[Double]` to SwiftUI for rendering dynamic core grid visualizations.
gronod added the Kind/Feature
Priority
Critical
1
labels 2026-09-08 10:18:33 +01:00
gronod added the Project/AntigravityFeature/BackendFeature/UI labels 2026-09-08 10:28:25 +01:00
Author
Owner

Technical Implementation Plan & Code Solution

1. Mach Host Processor Load Architecture

MacOS Mach kernel exposes fine-grained tick counters for every logical processor core via host_processor_info with PROCESSOR_CPU_LOAD_INFO.

Tick categories tracked per core:

  • CPU_STATE_USER
  • CPU_STATE_SYSTEM
  • CPU_STATE_IDLE
  • CPU_STATE_NICE

2. Objective-C Provider (MMCPUUsageProvider.m)

#import "MMCPUUsageProvider.h"
#import <mach/mach.h>
#import <mach/processor_info.h>
#import <mach/mach_host.h>
#import <sys/sysctl.h>

@interface MMCPUUsageProvider () {
    processor_cpu_load_info_t _prevCpuLoadInfo;
    mach_msg_type_number_t _prevCpuMsgCount;
    natural_t _processorCount;
    os_unfair_lock _lock;
}
@end

@implementation MMCPUUsageProvider

- (instancetype)init {
    self = [super init];
    if (self) {
        _lock = OS_UNFAIR_LOCK_INIT;
        _prevCpuLoadInfo = NULL;
        _prevCpuMsgCount = 0;
        _processorCount = 0;
    }
    return self;
}

- (void)dealloc {
    if (_prevCpuLoadInfo != NULL) {
        vm_deallocate(mach_task_self(), (vm_address_t)_prevCpuLoadInfo, _prevCpuMsgCount * sizeof(integer_t));
    }
}

- (uint64_t)queryCPUFrequencyHz {
    uint64_t freq = 0;
    size_t size = sizeof(freq);
    if (sysctlbyname("hw.cpufrequency", &freq, &size, NULL, 0) == 0 && freq > 0) {
        return freq;
    }
    return 0;
}

- (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error {
    os_unfair_lock_lock(&_lock);

    natural_t cpuCount = 0;
    processor_info_array_t cpuInfo = NULL;
    mach_msg_type_number_t cpuMsgCount = 0;

    kern_return_t kr = host_processor_info(
        mach_host_self(),
        PROCESSOR_CPU_LOAD_INFO,
        &cpuCount,
        &cpuInfo,
        &cpuMsgCount
    );

    if (kr != KERN_SUCCESS || cpuInfo == NULL) {
        os_unfair_lock_unlock(&_lock);
        return @{};
    }

    processor_cpu_load_info_t currentLoad = (processor_cpu_load_info_t)cpuInfo;

    // First sample initialization
    if (_prevCpuLoadInfo == NULL) {
        _prevCpuLoadInfo = currentLoad;
        _prevCpuMsgCount = cpuMsgCount;
        _processorCount = cpuCount;
        os_unfair_lock_unlock(&_lock);
        return @{@"initialized": @(YES)};
    }

    NSMutableArray<NSDictionary *> *cores = [NSMutableArray arrayWithCapacity:cpuCount];
    uint64_t totalUser = 0, totalSystem = 0, totalIdle = 0, totalNice = 0;

    for (natural_t i = 0; i < cpuCount; i++) {
        uint64_t userDiff = currentLoad[i].cpu_ticks[CPU_STATE_USER] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_USER];
        uint64_t sysDiff  = currentLoad[i].cpu_ticks[CPU_STATE_SYSTEM] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_SYSTEM];
        uint64_t idleDiff = currentLoad[i].cpu_ticks[CPU_STATE_IDLE] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_IDLE];
        uint64_t niceDiff = currentLoad[i].cpu_ticks[CPU_STATE_NICE] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_NICE];

        uint64_t totalTicks = userDiff + sysDiff + idleDiff + niceDiff;
        double coreUsage = (totalTicks > 0) ? ((double)(userDiff + sysDiff + niceDiff) / (double)totalTicks) * 100.0 : 0.0;

        [cores addObject:@{
            @"coreIndex": @(i),
            @"usagePercentage": @(coreUsage),
            @"userPercentage": @(totalTicks > 0 ? ((double)userDiff / totalTicks) * 100.0 : 0.0),
            @"systemPercentage": @(totalTicks > 0 ? ((double)sysDiff / totalTicks) * 100.0 : 0.0)
        }];

        totalUser += userDiff;
        totalSystem += sysDiff;
        totalIdle += idleDiff;
        totalNice += niceDiff;
    }

    // Free previous buffer and assign current buffer
    vm_deallocate(mach_task_self(), (vm_address_t)_prevCpuLoadInfo, _prevCpuMsgCount * sizeof(integer_t));
    _prevCpuLoadInfo = currentLoad;
    _prevCpuMsgCount = cpuMsgCount;
    _processorCount = cpuCount;

    os_unfair_lock_unlock(&_lock);

    uint64_t aggregateTicks = totalUser + totalSystem + totalIdle + totalNice;
    double aggregateUsage = (aggregateTicks > 0) ? ((double)(totalUser + totalSystem + totalNice) / (double)aggregateTicks) * 100.0 : 0.0;

    return @{
        @"aggregateUsage": @(aggregateUsage),
        @"userUsage": @(aggregateTicks > 0 ? ((double)totalUser / aggregateTicks) * 100.0 : 0.0),
        @"systemUsage": @(aggregateTicks > 0 ? ((double)totalSystem / aggregateTicks) * 100.0 : 0.0),
        @"coreCount": @(cpuCount),
        @"frequencyHz": @([self queryCPUFrequencyHz]),
        @"cores": [cores copy]
    };
}
@end

3. Swift UI Integration

public struct CoreLoad: Identifiable, Sendable {
    public let id: Int
    public let usagePercentage: Double
}

@Observable
public final class CPUSnapshot {
    public var totalUsage: Double = 0.0
    public var userUsage: Double = 0.0
    public var systemUsage: Double = 0.0
    public var frequencyGHz: Double = 0.0
    public var coreLoads: [CoreLoad] = []

    public func update(from dict: [String: Any]) {
        self.totalUsage = dict["aggregateUsage"] as? Double ?? 0.0
        self.userUsage = dict["userUsage"] as? Double ?? 0.0
        self.systemUsage = dict["systemUsage"] as? Double ?? 0.0

        if let freqHz = dict["frequencyHz"] as? UInt64, freqHz > 0 {
            self.frequencyGHz = Double(freqHz) / 1_000_000_000.0
        }

        if let coresData = dict["cores"] as? [[String: Any]] {
            self.coreLoads = coresData.compactMap { item in
                guard let idx = item["coreIndex"] as? Int,
                      let usage = item["usagePercentage"] as? Double else { return nil }
                return CoreLoad(id: idx, usagePercentage: usage)
            }
        }
    }
}

4. Critical Memory Management Detail

Every call to host_processor_info() allocates a kernel Mach virtual memory buffer cpuInfo. It is mandatory to invoke vm_deallocate() on each previous pointer to prevent virtual memory leak accumulation over long runtimes.

## Technical Implementation Plan & Code Solution ### 1. Mach Host Processor Load Architecture MacOS Mach kernel exposes fine-grained tick counters for every logical processor core via `host_processor_info` with `PROCESSOR_CPU_LOAD_INFO`. Tick categories tracked per core: - `CPU_STATE_USER` - `CPU_STATE_SYSTEM` - `CPU_STATE_IDLE` - `CPU_STATE_NICE` --- ### 2. Objective-C Provider (`MMCPUUsageProvider.m`) ```objc #import "MMCPUUsageProvider.h" #import <mach/mach.h> #import <mach/processor_info.h> #import <mach/mach_host.h> #import <sys/sysctl.h> @interface MMCPUUsageProvider () { processor_cpu_load_info_t _prevCpuLoadInfo; mach_msg_type_number_t _prevCpuMsgCount; natural_t _processorCount; os_unfair_lock _lock; } @end @implementation MMCPUUsageProvider - (instancetype)init { self = [super init]; if (self) { _lock = OS_UNFAIR_LOCK_INIT; _prevCpuLoadInfo = NULL; _prevCpuMsgCount = 0; _processorCount = 0; } return self; } - (void)dealloc { if (_prevCpuLoadInfo != NULL) { vm_deallocate(mach_task_self(), (vm_address_t)_prevCpuLoadInfo, _prevCpuMsgCount * sizeof(integer_t)); } } - (uint64_t)queryCPUFrequencyHz { uint64_t freq = 0; size_t size = sizeof(freq); if (sysctlbyname("hw.cpufrequency", &freq, &size, NULL, 0) == 0 && freq > 0) { return freq; } return 0; } - (NSDictionary<NSString *, id> *)sampleTelemetryWithError:(NSError **)error { os_unfair_lock_lock(&_lock); natural_t cpuCount = 0; processor_info_array_t cpuInfo = NULL; mach_msg_type_number_t cpuMsgCount = 0; kern_return_t kr = host_processor_info( mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpuCount, &cpuInfo, &cpuMsgCount ); if (kr != KERN_SUCCESS || cpuInfo == NULL) { os_unfair_lock_unlock(&_lock); return @{}; } processor_cpu_load_info_t currentLoad = (processor_cpu_load_info_t)cpuInfo; // First sample initialization if (_prevCpuLoadInfo == NULL) { _prevCpuLoadInfo = currentLoad; _prevCpuMsgCount = cpuMsgCount; _processorCount = cpuCount; os_unfair_lock_unlock(&_lock); return @{@"initialized": @(YES)}; } NSMutableArray<NSDictionary *> *cores = [NSMutableArray arrayWithCapacity:cpuCount]; uint64_t totalUser = 0, totalSystem = 0, totalIdle = 0, totalNice = 0; for (natural_t i = 0; i < cpuCount; i++) { uint64_t userDiff = currentLoad[i].cpu_ticks[CPU_STATE_USER] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_USER]; uint64_t sysDiff = currentLoad[i].cpu_ticks[CPU_STATE_SYSTEM] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_SYSTEM]; uint64_t idleDiff = currentLoad[i].cpu_ticks[CPU_STATE_IDLE] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_IDLE]; uint64_t niceDiff = currentLoad[i].cpu_ticks[CPU_STATE_NICE] - _prevCpuLoadInfo[i].cpu_ticks[CPU_STATE_NICE]; uint64_t totalTicks = userDiff + sysDiff + idleDiff + niceDiff; double coreUsage = (totalTicks > 0) ? ((double)(userDiff + sysDiff + niceDiff) / (double)totalTicks) * 100.0 : 0.0; [cores addObject:@{ @"coreIndex": @(i), @"usagePercentage": @(coreUsage), @"userPercentage": @(totalTicks > 0 ? ((double)userDiff / totalTicks) * 100.0 : 0.0), @"systemPercentage": @(totalTicks > 0 ? ((double)sysDiff / totalTicks) * 100.0 : 0.0) }]; totalUser += userDiff; totalSystem += sysDiff; totalIdle += idleDiff; totalNice += niceDiff; } // Free previous buffer and assign current buffer vm_deallocate(mach_task_self(), (vm_address_t)_prevCpuLoadInfo, _prevCpuMsgCount * sizeof(integer_t)); _prevCpuLoadInfo = currentLoad; _prevCpuMsgCount = cpuMsgCount; _processorCount = cpuCount; os_unfair_lock_unlock(&_lock); uint64_t aggregateTicks = totalUser + totalSystem + totalIdle + totalNice; double aggregateUsage = (aggregateTicks > 0) ? ((double)(totalUser + totalSystem + totalNice) / (double)aggregateTicks) * 100.0 : 0.0; return @{ @"aggregateUsage": @(aggregateUsage), @"userUsage": @(aggregateTicks > 0 ? ((double)totalUser / aggregateTicks) * 100.0 : 0.0), @"systemUsage": @(aggregateTicks > 0 ? ((double)totalSystem / aggregateTicks) * 100.0 : 0.0), @"coreCount": @(cpuCount), @"frequencyHz": @([self queryCPUFrequencyHz]), @"cores": [cores copy] }; } @end ``` --- ### 3. Swift UI Integration ```swift public struct CoreLoad: Identifiable, Sendable { public let id: Int public let usagePercentage: Double } @Observable public final class CPUSnapshot { public var totalUsage: Double = 0.0 public var userUsage: Double = 0.0 public var systemUsage: Double = 0.0 public var frequencyGHz: Double = 0.0 public var coreLoads: [CoreLoad] = [] public func update(from dict: [String: Any]) { self.totalUsage = dict["aggregateUsage"] as? Double ?? 0.0 self.userUsage = dict["userUsage"] as? Double ?? 0.0 self.systemUsage = dict["systemUsage"] as? Double ?? 0.0 if let freqHz = dict["frequencyHz"] as? UInt64, freqHz > 0 { self.frequencyGHz = Double(freqHz) / 1_000_000_000.0 } if let coresData = dict["cores"] as? [[String: Any]] { self.coreLoads = coresData.compactMap { item in guard let idx = item["coreIndex"] as? Int, let usage = item["usagePercentage"] as? Double else { return nil } return CoreLoad(id: idx, usagePercentage: usage) } } } } ``` --- ### 4. Critical Memory Management Detail Every call to `host_processor_info()` allocates a kernel Mach virtual memory buffer `cpuInfo`. It is mandatory to invoke `vm_deallocate()` on each previous pointer to prevent virtual memory leak accumulation over long runtimes.
gronod added this to the M2: Primary System & Thermal Telemetry milestone 2026-09-08 10:42:03 +01:00
Author
Owner

Completed in feat/5-cpu-load and merged into develop.

  • Implemented MMCPULoadProvider querying Mach kernel host_processor_info (PROCESSOR_CPU_LOAD_INFO).
  • Implemented delta tick calculation across user, system, idle, nice states.
  • Implemented CPU frequency polling (hw.cpufrequency_max) and thermal throttling detection.
  • Unit tested with 100% pass in MMCPULoadTests.swift.
Completed in `feat/5-cpu-load` and merged into `develop`. - Implemented `MMCPULoadProvider` querying Mach kernel `host_processor_info` (`PROCESSOR_CPU_LOAD_INFO`). - Implemented delta tick calculation across user, system, idle, nice states. - Implemented CPU frequency polling (`hw.cpufrequency_max`) and thermal throttling detection. - Unit tested with 100% pass in `MMCPULoadTests.swift`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: gronod/MacMonitor#5