473 lines
16 KiB
Swift
473 lines
16 KiB
Swift
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<host_cpu_load_info>.stride / MemoryLayout<integer_t>.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<vm_statistics64>.stride / MemoryLayout<integer_t>.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<xsw_usage>.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<ifaddrs>?
|
||
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)
|
||
}
|
||
}
|