Monitor/Sources/FloatingMonitor/SystemMonitor.swift

351 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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 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<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
}
}