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] // 网络速度 var networkDownloadBytesPerSec: UInt64 var networkUploadBytesPerSec: UInt64 var networkInterface: String // 磁盘 I/O var diskReadBytesPerSec: UInt64 var diskWriteBytesPerSec: UInt64 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: [], networkDownloadBytesPerSec: 0, networkUploadBytesPerSec: 0, networkInterface: "", diskReadBytesPerSec: 0, diskWriteBytesPerSec: 0 ) private var timer: Timer? private var previousCPU: host_cpu_load_info? // 网络 / 磁盘上次采样 private var previousNetwork: (inBytes: UInt64, outBytes: UInt64)? private var previousNetworkTime: Date? private var previousDisk: (readBytes: UInt64, writeBytes: UInt64)? private var previousDiskTime: Date? // 采集频率 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) let net = fetchNetwork() let disk = fetchDiskIO() 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, networkDownloadBytesPerSec: net.download, networkUploadBytesPerSec: net.upload, networkInterface: net.interface, diskReadBytesPerSec: disk.read, diskWriteBytesPerSec: disk.write ) } // 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 pageSizeU64 = UInt64(pageSize) let cachedFiles = (UInt64(vmStat.external_page_count) + UInt64(vmStat.purgeable_count)) * pageSizeU64 let freeBytes = UInt64(vmStat.free_count) * pageSizeU64 let usedRaw = total - cachedFiles - freeBytes 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 } // MARK: - 网络速度 (getifaddrs) private func fetchNetwork() -> (download: UInt64, upload: UInt64, interface: String) { var ifaddr: UnsafeMutablePointer? guard getifaddrs(&ifaddr) == 0, let first = ifaddr else { return (0, 0, "") } defer { freeifaddrs(first) } var inBytes: UInt64 = 0 var outBytes: UInt64 = 0 var ifName = "" var ptr = first while true { let name = String(cString: ptr.pointee.ifa_name) // 优先 Wi-Fi,其次以太网 if (name == "en0" || name.hasPrefix("en")) && Int32(ptr.pointee.ifa_addr?.pointee.sa_family ?? 0) == AF_LINK { let data = ptr.pointee.ifa_data?.assumingMemoryBound(to: if_data.self) if let d = data { let ib = UInt64(d.pointee.ifi_ibytes) let ob = UInt64(d.pointee.ifi_obytes) // en0(Wi-Fi)优先级最高 if name == "en0" || (ib + ob) > (inBytes + outBytes) { inBytes = ib outBytes = ob ifName = name } } } guard let next = ptr.pointee.ifa_next else { break } ptr = next } let now = Date() defer { previousNetwork = (inBytes, outBytes) previousNetworkTime = now } guard let prev = previousNetwork, let prevTime = previousNetworkTime else { return (0, 0, ifName) } let elapsed = max(now.timeIntervalSince(prevTime), 0.1) let dl = UInt64(max(0, Double(inBytes) - Double(prev.inBytes)) / elapsed) let ul = UInt64(max(0, Double(outBytes) - Double(prev.outBytes)) / elapsed) return (dl, ul, ifName) } // MARK: - 磁盘 I/O (IOKit IOBlockStorageDriver) private func fetchDiskIO() -> (read: UInt64, write: UInt64) { var readBytes: UInt64 = 0 var writeBytes: UInt64 = 0 var iterator: io_iterator_t = 0 let matchDict = IOServiceMatching("IOBlockStorageDriver") guard IOServiceGetMatchingServices(kIOMainPortDefault, matchDict, &iterator) == KERN_SUCCESS else { return (0, 0) } defer { IOObjectRelease(iterator) } var entry = IOIteratorNext(iterator) while entry != 0 { if let statsCF = IORegistryEntryCreateCFProperty(entry, "Statistics" as CFString, kCFAllocatorDefault, 0) { let stats = statsCF.takeUnretainedValue() as? [String: Any] if let r = stats?["Bytes (Read)"] as? UInt64 { readBytes += r } else if let r = stats?["Bytes (Read)"] as? Int { readBytes += UInt64(r) } if let w = stats?["Bytes (Write)"] as? UInt64 { writeBytes += w } else if let w = stats?["Bytes (Write)"] as? Int { writeBytes += UInt64(w) } } IOObjectRelease(entry) entry = IOIteratorNext(iterator) } let now = Date() defer { previousDisk = (readBytes, writeBytes) previousDiskTime = now } guard let prev = previousDisk, let prevTime = previousDiskTime else { return (0, 0) } let elapsed = max(now.timeIntervalSince(prevTime), 0.1) let r = UInt64(max(0, Double(readBytes) - Double(prev.readBytes)) / elapsed) let w = UInt64(max(0, Double(writeBytes) - Double(prev.writeBytes)) / elapsed) return (r, w) } }