From 5e8343dea865cfb1b832ea69e06b61f24d4db899 Mon Sep 17 00:00:00 2001 From: hariel1985 Date: Sat, 31 Jan 2026 16:37:38 +0100 Subject: [PATCH] Add CPU/Total column showing normalized system-wide CPU usage - Add cpuUsageTotal property to ProcessItem (100% = all cores) - Keep cpuUsage as per-core percentage (100% = 1 core) - Display both columns: CPU/Core and CPU/Total - CPU/Total values now sum to match the total system CPU This clarifies the difference between per-core and system-wide CPU measurements that was causing confusion. Co-Authored-By: Claude Opus 4.5 --- TopManager/Models/ProcessInfo.swift | 6 +++- TopManager/Services/ProcessMonitor.swift | 6 ++++ .../Views/Processes/ProcessDetailView.swift | 1 + TopManager/Views/Processes/ProcessView.swift | 28 +++++++++++++++++-- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/TopManager/Models/ProcessInfo.swift b/TopManager/Models/ProcessInfo.swift index f3be56b..789775b 100644 --- a/TopManager/Models/ProcessInfo.swift +++ b/TopManager/Models/ProcessInfo.swift @@ -6,7 +6,8 @@ struct ProcessItem: Identifiable, Hashable { let pid: pid_t let name: String let user: String - let cpuUsage: Double + let cpuUsage: Double // Per-core: 100% = 1 core fully utilized + let cpuUsageTotal: Double // Normalized: 100% = all cores fully utilized let memoryUsage: Int64 let threadCount: Int32 let state: ProcessState @@ -21,6 +22,7 @@ struct ProcessItem: Identifiable, Hashable { name: String, user: String, cpuUsage: Double, + cpuUsageTotal: Double, memoryUsage: Int64, threadCount: Int32, state: ProcessState, @@ -33,6 +35,7 @@ struct ProcessItem: Identifiable, Hashable { self.name = name self.user = user self.cpuUsage = cpuUsage + self.cpuUsageTotal = cpuUsageTotal self.memoryUsage = memoryUsage self.threadCount = threadCount self.state = state @@ -48,6 +51,7 @@ struct ProcessItem: Identifiable, Hashable { static func == (lhs: ProcessItem, rhs: ProcessItem) -> Bool { lhs.pid == rhs.pid && lhs.cpuUsage == rhs.cpuUsage && + lhs.cpuUsageTotal == rhs.cpuUsageTotal && lhs.memoryUsage == rhs.memoryUsage && lhs.threadCount == rhs.threadCount && lhs.state == rhs.state diff --git a/TopManager/Services/ProcessMonitor.swift b/TopManager/Services/ProcessMonitor.swift index f5fa957..abcc421 100644 --- a/TopManager/Services/ProcessMonitor.swift +++ b/TopManager/Services/ProcessMonitor.swift @@ -11,6 +11,7 @@ final class ProcessMonitor { private var userCache: [uid_t: String] = [:] private let timebaseInfo: mach_timebase_info_data_t private var refreshCounter = 0 + private let processorCount = Double(ProcessInfo.processInfo.processorCount) init() { var info = mach_timebase_info_data_t() @@ -111,11 +112,15 @@ final class ProcessMonitor { let icon = fetchIcon(pid: pid) + // Calculate normalized CPU (100% = all cores) + let cpuUsageTotal = cpuUsage / processorCount + return ProcessItem( pid: pid, name: name, user: user, cpuUsage: cpuUsage, + cpuUsageTotal: cpuUsageTotal, memoryUsage: memoryUsage, threadCount: threadCount, state: state, @@ -240,6 +245,7 @@ final class ProcessMonitor { name: name, user: user, cpuUsage: 0, + cpuUsageTotal: 0, memoryUsage: 0, threadCount: 0, state: state, diff --git a/TopManager/Views/Processes/ProcessDetailView.swift b/TopManager/Views/Processes/ProcessDetailView.swift index 54c79f7..b439935 100644 --- a/TopManager/Views/Processes/ProcessDetailView.swift +++ b/TopManager/Views/Processes/ProcessDetailView.swift @@ -100,6 +100,7 @@ struct DetailRow: View { name: "Safari", user: "ariel", cpuUsage: 12.5, + cpuUsageTotal: 1.56, memoryUsage: 512 * 1024 * 1024, threadCount: 42, state: .running, diff --git a/TopManager/Views/Processes/ProcessView.swift b/TopManager/Views/Processes/ProcessView.swift index d57eb7d..bc8e7c8 100644 --- a/TopManager/Views/Processes/ProcessView.swift +++ b/TopManager/Views/Processes/ProcessView.swift @@ -1,7 +1,7 @@ import SwiftUI enum ProcessSortColumn: String { - case name, pid, cpu, memory, threads, user, state + case name, pid, cpu, cpuTotal, memory, threads, user, state } struct ProcessView: View { @@ -42,6 +42,8 @@ struct ProcessView: View { comparison = lhs.pid < rhs.pid ? .orderedAscending : (lhs.pid > rhs.pid ? .orderedDescending : .orderedSame) case .cpu: comparison = lhs.cpuUsage < rhs.cpuUsage ? .orderedAscending : (lhs.cpuUsage > rhs.cpuUsage ? .orderedDescending : .orderedSame) + case .cpuTotal: + comparison = lhs.cpuUsageTotal < rhs.cpuUsageTotal ? .orderedAscending : (lhs.cpuUsageTotal > rhs.cpuUsageTotal ? .orderedDescending : .orderedSame) case .memory: comparison = lhs.memoryUsage < rhs.memoryUsage ? .orderedAscending : (lhs.memoryUsage > rhs.memoryUsage ? .orderedDescending : .orderedSame) case .threads: @@ -100,13 +102,20 @@ struct ProcessView: View { } .width(60) - TableColumn("CPU %", value: \.cpuUsage) { process in + TableColumn("CPU/Core", value: \.cpuUsage) { process in Text(String(format: "%.1f%%", process.cpuUsage)) .monospacedDigit() .foregroundColor(cpuColor(process.cpuUsage)) } .width(70) + TableColumn("CPU/Total", value: \.cpuUsageTotal) { process in + Text(String(format: "%.2f%%", process.cpuUsageTotal)) + .monospacedDigit() + .foregroundColor(cpuColorTotal(process.cpuUsageTotal)) + } + .width(70) + TableColumn("Memory", value: \.memoryUsage) { process in Text(formatBytes(process.memoryUsage)) .monospacedDigit() @@ -186,7 +195,9 @@ struct ProcessView: View { // Use string representation of keypath to determine column let keyPathString = String(describing: comparator) - if keyPathString.contains("cpuUsage") { + if keyPathString.contains("cpuUsageTotal") { + sortColumn = .cpuTotal + } else if keyPathString.contains("cpuUsage") { sortColumn = .cpu } else if keyPathString.contains("memoryUsage") { sortColumn = .memory @@ -274,6 +285,17 @@ struct ProcessView: View { return .primary } + private func cpuColorTotal(_ usage: Double) -> Color { + if usage > 10 { + return .red + } else if usage > 5 { + return .orange + } else if usage > 2 { + return .yellow + } + return .primary + } + private func stateColor(_ state: ProcessState) -> Color { switch state { case .running: return .green