thefearsomeshrub
thefearsomeshrub

Reputation: 3

How can I retrieve the CPU usage of my Mac in the form of a percentage and put that into an updating variable?

I'm coding a Mac app in SwiftUI 6.0.3 and Xcode 16. My Mac is up to date with macOS Sequoia 15.3.1. I'm trying to have a menu bar item that updates at an interval with the percentage of the CPU that I am using. This code returns no errors, and as far as I can tell, should work, but I must be missing something. When I run the app, instead of giving me a percentage, it just says "Calculating..." which is the default value of the cpuUsage variable.

import SwiftUI
import Foundation

@main
struct MenuBarApp: App {
    @State private var cpuUsage: String = "Calculating..."
    
    var body: some Scene {
        // Menu bar item
        MenuBarExtra("icon \(cpuUsage)") {
            // Option to quit app
            Button("Quit") {
                NSApp.terminate(nil)
            }
        }
    }
    
    // Starts repeating CPU monitoring function at an interval of 1 second
    func startCPUMonitoring() {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            if let usage = getCPUUsage() {
                cpuUsage = String(format: "%.1f%%", usage)
            } else {
                cpuUsage = "N/A"
            }
        }
    }

    // Retrieves CPU usage as a percentage of the total
    func getCPUUsage() -> Double? {
        var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
        var cpuLoad = host_cpu_load_info()
        let result = withUnsafeMutablePointer(to: &cpuLoad) {
            $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
                host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
            }
        }
        
        guard result == KERN_SUCCESS else {
            print("Error retrieving CPU load: \(result)")
            return nil
        }
        
        let user = Double(cpuLoad.cpu_ticks.0)
        let system = Double(cpuLoad.cpu_ticks.1)
        let idle = Double(cpuLoad.cpu_ticks.2)
        let nice = Double(cpuLoad.cpu_ticks.3)
        
        let totalTicks = user + system + idle + nice
        let cpuUsage = (user + system + nice) / totalTicks * 100.0
        
        return cpuUsage
    }
}

I've asked ChatGPT and went through the Apple Developer documentation but the problem is so niche that I can't find a single relatively recent source that discusses anything remotely similar. I think it's probably an issue with what function is being called where or the order of events or something, but I can't figure it out. Please help. I'm just trying to make this work on the latest version of macOS, it doesn't matter if it works for older versions.

Upvotes: 0

Views: 63

Answers (1)

Assuming your calculations are correct, try this approach using a Button("Start") to start monitoring, and a more precise String(format: "%.6f%%", usage) to show any differences in the value, as shown in this example code.

Note you need to click on the "Start" button to display the results.

@main
struct MenuBarApp: App {
    @State private var cpuUsage: String = "Calculating..."
    
    var body: some Scene {
        // Menu bar item
        MenuBarExtra("icon \(cpuUsage)") {
            Button("Start") {
                startCPUMonitoring()  // <--- here to start
            }
            Button("Quit") {
                NSApp.terminate(nil)
            }
        }
    }
    
    // Starts repeating CPU monitoring function at an interval of 1 second
    func startCPUMonitoring() {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            if let usage = getCPUUsage() {
                cpuUsage = String(format: "%.6f%%", usage)  // <--- here
            } else {
                cpuUsage = "N/A"
            }
        }
    }

    // Retrieves CPU usage as a percentage of the total
    func getCPUUsage() -> Double? {
        var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
        var cpuLoad = host_cpu_load_info()
        let result = withUnsafeMutablePointer(to: &cpuLoad) {
            $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
                host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
            }
        }
        
        guard result == KERN_SUCCESS else {
            print("Error retrieving CPU load: \(result)")
            return nil
        }
        
        let user = Double(cpuLoad.cpu_ticks.0)
        let system = Double(cpuLoad.cpu_ticks.1)
        let idle = Double(cpuLoad.cpu_ticks.2)
        let nice = Double(cpuLoad.cpu_ticks.3)
        
        let totalTicks = user + system + idle + nice
        let cpuUsage = (user + system + nice) / totalTicks * 100.0
        
        return cpuUsage
    }
}

EDIT-1:

If you want to start the CPU monitoring immediately without having to click "Start", then try this approach using a @Observable class CPUMonitor. This will observe any changes in the cpuUsage and update the View/MenuBar.

@Observable class CPUMonitor {
    var cpuUsage: String = "Calculating..."
    
    init() {
        startCPUMonitoring()  // <--- here
    }
    
    // Starts repeating CPU monitoring function at an interval of 1 second
    func startCPUMonitoring() {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            if let usage = self.getCPUUsage() {
                self.cpuUsage = String(format: "%.6f%%", usage)  // <--- here
            } else {
                self.cpuUsage = "N/A"
            }
        }
    }

    // Retrieves CPU usage as a percentage of the total
    func getCPUUsage() -> Double? {
        var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
        var cpuLoad = host_cpu_load_info()
        let result = withUnsafeMutablePointer(to: &cpuLoad) {
            $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
                host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
            }
        }
        
        guard result == KERN_SUCCESS else {
            print("Error retrieving CPU load: \(result)")
            return nil
        }
        
        let user = Double(cpuLoad.cpu_ticks.0)
        let system = Double(cpuLoad.cpu_ticks.1)
        let idle = Double(cpuLoad.cpu_ticks.2)
        let nice = Double(cpuLoad.cpu_ticks.3)
        
        let totalTicks = user + system + idle + nice
        let cpuUsage = (user + system + nice) / totalTicks * 100.0
        
        return cpuUsage
    }
}

@main
struct MenuBarApp: App {
    let cpuMonitor = CPUMonitor() // <--- here
    
    var body: some Scene {
        MenuBarExtra("icon \(cpuMonitor.cpuUsage)") {  // <--- here
            Button("Quit") {
                NSApp.terminate(nil)
            }
        }
    }
}

Works well for me, tested on macOS 15.3.1, using Xcode 16.2.

Upvotes: 1

Related Questions