import Foundation import IOKit import Darwin // MARK: - 系统信息数据结构 struct SystemInfo { var cpuUsage: Double // 0-100 var cpuCores: Int var cpuModel: String var gpuUsage: Double? // 0-100 (部分 Mac 可能为 nil) var gpuModel: String var memoryTotal: UInt64 // bytes var memoryUsed: UInt64 var memoryUsagePercent: Double var swapTotal: UInt64 var swapUsed: UInt64 var swapUsagePercent: Double var swapIns: UInt64 var swapOuts: UInt64 var cpuTemperature: Double? // °C var gpuTemperature: Double? // °C var topCPUProcesses: [ProcessInfo] var topMemoryProcesses: [ProcessInfo] struct ProcessInfo: Identifiable { let id = UUID() let pid: Int32 let name: String let cpuPercent: Double let memoryBytes: UInt64 } } // MARK: - 系统数据采集引擎 @MainActor final class SystemMonitor: ObservableObject { @Published var info = SystemInfo( cpuUsage: 0, cpuCores: 0, cpuModel: "", gpuUsage: nil, gpuModel: "", memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0, swapTotal: 0, swapUsed: 0, swapUsagePercent: 0, swapIns: 0, swapOuts: 0, cpuTemperature: nil, gpuTemperature: nil, topCPUProcesses: [], topMemoryProcesses: [] ) private var timer: Timer? private var previousCPU: host_cpu_load_info? // 采集频率 var interval: TimeInterval = 1.5 { didSet { restartTimer() } } var topN: Int = 5 nonisolated init() {} // MARK: - 启动 / 停止 func start() { fetch() // 立即拉一次 restartTimer() } func stop() { timer?.invalidate() timer = nil } private func restartTimer() { timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in Task { @MainActor in self?.fetch() } } // 允许 timer 在 tracking 模式下也触发(防止拖拽窗口时卡顿不更新) if let t = timer { RunLoop.current.add(t, forMode: .common) } } // MARK: - 数据采集 func fetch() { let cpu = fetchCPU() let gpu = fetchGPU() let mem = fetchMemory() let swap = fetchSwap() let temp = fetchTemperature() let procs = fetchTopProcesses(count: topN) info = SystemInfo( cpuUsage: cpu.usage, cpuCores: cpu.cores, cpuModel: cpu.model, gpuUsage: gpu.usage, gpuModel: gpu.model, memoryTotal: mem.total, memoryUsed: mem.used, memoryUsagePercent: mem.percent, swapTotal: swap.total, swapUsed: swap.used, swapUsagePercent: swap.percent, swapIns: swap.ins, swapOuts: swap.outs, cpuTemperature: temp.cpu, gpuTemperature: temp.gpu, topCPUProcesses: procs.cpu, topMemoryProcesses: procs.mem ) } // MARK: - CPU private func fetchCPU() -> (usage: Double, cores: Int, model: String) { var size = mach_msg_type_number_t(MemoryLayout.stride / MemoryLayout.stride) var info = host_cpu_load_info() let result = withUnsafeMutablePointer(to: &info) { $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 { return (0, ProcessInfo.processInfo.activeProcessorCount, "Unknown") } let cores = ProcessInfo.processInfo.activeProcessorCount // 获取 CPU 型号 var model = "Apple Silicon" var sysctlSize = 0 sysctlbyname("machdep.cpu.brand_string", nil, &sysctlSize, nil, 0) if sysctlSize > 0 { var brand = [CChar](repeating: 0, count: sysctlSize) sysctlbyname("machdep.cpu.brand_string", &brand, &sysctlSize, nil, 0) model = String(cString: brand) } let user = Double(info.cpu_ticks.0) let system = Double(info.cpu_ticks.1) let idle = Double(info.cpu_ticks.2) let nice = Double(info.cpu_ticks.3) var usage: Double = 0 if let prev = previousCPU { let prevUser = Double(prev.cpu_ticks.0) let prevSystem = Double(prev.cpu_ticks.1) let prevIdle = Double(prev.cpu_ticks.2) let prevNice = Double(prev.cpu_ticks.3) let totalTicks = (user - prevUser) + (system - prevSystem) + (idle - prevIdle) + (nice - prevNice) let usedTicks = (user - prevUser) + (system - prevSystem) + (nice - prevNice) if totalTicks > 0 { usage = (usedTicks / totalTicks) * 100 } } previousCPU = info return (min(usage, 100), cores, model) } // MARK: - GPU (通过 IOKit) private func fetchGPU() -> (usage: Double?, model: String) { var model = "Unknown GPU" var usage: Double? = nil // 查找 GPU 节点 var iterator: io_iterator_t = 0 let matchDict = IOServiceMatching("IOAccelerator") let result = IOServiceGetMatchingServices(kIOMainPortDefault, matchDict, &iterator) guard result == KERN_SUCCESS else { return (nil, "Unknown GPU") } var gpuEntry = IOIteratorNext(iterator) defer { IOObjectRelease(iterator) } while gpuEntry != 0 { // GPU 型号 if let modelCF = IORegistryEntryCreateCFProperty(gpuEntry, "model" as CFString, kCFAllocatorDefault, 0) { if let modelStr = (modelCF.takeUnretainedValue() as? String) ?? (modelCF.takeUnretainedValue() as? Data).flatMap({ String(data: $0, encoding: .utf8) }) { model = modelStr } } // 尝试读取性能统计 if let perfCF = IORegistryEntryCreateCFProperty(gpuEntry, "PerformanceStatistics" as CFString, kCFAllocatorDefault, 0) { let perfDict = perfCF.takeUnretainedValue() as? [String: Any] // Apple Silicon 常见 key if let util = perfDict?["GPU Core Utilization"] as? Double { usage = util * 100 } else if let util = perfDict?["gpuCoreUtilization"] as? Double { usage = util * 100 } else if let util = perfDict?["Device Utilization %"] as? Int { usage = Double(util) } else if let util = perfDict?["GPU Activity(%)"] as? Int { usage = Double(util) } } IOObjectRelease(gpuEntry) gpuEntry = IOIteratorNext(iterator) } // 如果没有从 IOKit 拿到型号,用 sysctl if model == "Unknown GPU" { var size = 0 sysctlbyname("hw.model", nil, &size, nil, 0) if size > 0 { var hwModel = [CChar](repeating: 0, count: size) sysctlbyname("hw.model", &hwModel, &size, nil, 0) let hw = String(cString: hwModel) if hw.hasPrefix("Mac") { // Apple Silicon 通常显示为 "Apple M1/M2/M3..." // 通过 IO 获取到的 model 更准确 } } } return (usage, model) } // MARK: - 内存 private func fetchMemory() -> (total: UInt64, used: UInt64, percent: Double) { let total = ProcessInfo.processInfo.physicalMemory var pageSize: vm_size_t = 0 host_page_size(mach_host_self(), &pageSize) var vmStat = vm_statistics64() var count = mach_msg_type_number_t(MemoryLayout.stride / MemoryLayout.stride) let result = withUnsafeMutablePointer(to: &vmStat) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &count) } } guard result == KERN_SUCCESS else { return (total, 0, 0) } let activePages = UInt64(vmStat.active_count) let wiredPages = UInt64(vmStat.wire_count) let compressedPages = UInt64(vmStat.compressor_page_count) let pageSizeU64 = UInt64(pageSize) let usedRaw = (activePages + wiredPages + compressedPages) * pageSizeU64 let percent = Double(usedRaw) / Double(total) * 100 return (total, usedRaw, min(percent, 100)) } // MARK: - Swap private func fetchSwap() -> (total: UInt64, used: UInt64, percent: Double, ins: UInt64, outs: UInt64) { var xsw = xsw_usage() var size = MemoryLayout.size let result = sysctlbyname("vm.swapusage", &xsw, &size, nil, 0) if result == 0 { return (xsw.xsu_total, xsw.xsu_used, xsw.xsu_total > 0 ? Double(xsw.xsu_used) / Double(xsw.xsu_total) * 100 : 0, 0, 0) } return (0, 0, 0, 0, 0) } // MARK: - 温度 (SMC) private func fetchTemperature() -> (cpu: Double?, gpu: Double?) { let cpu = SMCSensor.shared.readCPUTemperature() let gpu = SMCSensor.shared.readGPUTemperature() return (cpu, gpu) } // MARK: - 进程列表 private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) { // 使用 ps 命令获取进程 CPU / 内存排行 // ps -eo pid,%cpu,rss,comm -r | head -n 30 let cpuProcs = executePS(sortByCPU: true, count: count) let memProcs = executePS(sortByCPU: false, count: count) return (cpuProcs, memProcs) } private func executePS(sortByCPU: Bool, count: Int) -> [SystemInfo.ProcessInfo] { let sortFlag = sortByCPU ? "-r" : "-m" let command = "/bin/ps -eo pid,%cpu,rss,comm \(sortFlag) 2>/dev/null | head -n \(count + 1)" let process = Process() process.launchPath = "/bin/bash" process.arguments = ["-c", command] let pipe = Pipe() process.standardOutput = pipe process.standardError = Pipe() do { try process.run() process.waitUntilExit() } catch { return [] } let data = pipe.fileHandleForReading.readDataToEndOfFile() guard let output = String(data: data, encoding: .utf8) else { return [] } var results: [SystemInfo.ProcessInfo] = [] let lines = output.components(separatedBy: "\n").dropFirst() // 去掉表头 for line in lines { let trimmed = line.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { continue } // 格式: PID %CPU RSS COMMAND var components = trimmed.components(separatedBy: .whitespaces) // 过滤空字符串 components = components.filter { !$0.isEmpty } guard components.count >= 3 else { continue } let pidStr = components[0] let cpuStr = components[1] let rssStr = components[2] let comm = components.dropFirst(3).joined(separator: " ") guard let pid = Int32(pidStr), let cpu = Double(cpuStr), let rss = UInt64(rssStr) else { continue } let rssBytes = rss * 1024 // RSS 单位是 KB results.append(SystemInfo.ProcessInfo( pid: pid, name: comm, cpuPercent: cpu, memoryBytes: rssBytes )) } return results } }