2ad21cad58
Features: - Real-time audio level monitoring via BlackHole virtual audio device - Classic VU meter display with dB scale (-60 to 0 dB) - Peak hold indicators with configurable hold time - System resource monitors: CPU, RAM, Disk, Network - SwiftUI interface with dark theme - Multi-device audio input selection - Settings window for configuration Built with AVAudioEngine for audio capture and Mach kernel APIs for system statistics.
279 lines
9.5 KiB
Swift
279 lines
9.5 KiB
Swift
//
|
|
// SystemMonitor.swift
|
|
// AudioVUMeter
|
|
//
|
|
// System resource monitoring for CPU, RAM, Disk, and Network
|
|
// Uses mach kernel APIs for accurate system statistics
|
|
//
|
|
|
|
import Foundation
|
|
import Darwin
|
|
|
|
/// System resource monitor class
|
|
class SystemMonitor: ObservableObject {
|
|
// MARK: - Published Properties
|
|
|
|
@Published var cpuUsage: Double = 0
|
|
@Published var memoryUsage: Double = 0
|
|
@Published var diskActivity: Double = 0
|
|
@Published var networkActivity: Double = 0
|
|
|
|
// Additional details
|
|
@Published var cpuUserUsage: Double = 0
|
|
@Published var cpuSystemUsage: Double = 0
|
|
@Published var memoryUsed: UInt64 = 0
|
|
@Published var memoryTotal: UInt64 = 0
|
|
@Published var networkBytesIn: UInt64 = 0
|
|
@Published var networkBytesOut: UInt64 = 0
|
|
|
|
// MARK: - Private Properties
|
|
|
|
private var updateTimer: Timer?
|
|
private var previousCPUInfo: host_cpu_load_info?
|
|
private var previousNetworkBytes: (in: UInt64, out: UInt64) = (0, 0)
|
|
private var previousDiskBytes: (read: UInt64, write: UInt64) = (0, 0)
|
|
|
|
private let updateInterval: TimeInterval = 0.5
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Start monitoring system resources
|
|
func startMonitoring() {
|
|
// Get initial values
|
|
previousCPUInfo = getCPULoadInfo()
|
|
previousNetworkBytes = getNetworkBytes()
|
|
previousDiskBytes = getDiskBytes()
|
|
|
|
// Start update timer
|
|
updateTimer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { [weak self] _ in
|
|
self?.updateMetrics()
|
|
}
|
|
|
|
// Initial update
|
|
updateMetrics()
|
|
}
|
|
|
|
/// Stop monitoring
|
|
func stopMonitoring() {
|
|
updateTimer?.invalidate()
|
|
updateTimer = nil
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
private func updateMetrics() {
|
|
DispatchQueue.global(qos: .background).async { [weak self] in
|
|
guard let self = self else { return }
|
|
|
|
let cpu = self.calculateCPUUsage()
|
|
let memory = self.calculateMemoryUsage()
|
|
let disk = self.calculateDiskActivity()
|
|
let network = self.calculateNetworkActivity()
|
|
|
|
DispatchQueue.main.async {
|
|
self.cpuUsage = cpu.total
|
|
self.cpuUserUsage = cpu.user
|
|
self.cpuSystemUsage = cpu.system
|
|
|
|
self.memoryUsage = memory.percentage
|
|
self.memoryUsed = memory.used
|
|
self.memoryTotal = memory.total
|
|
|
|
self.diskActivity = disk
|
|
self.networkActivity = network.percentage
|
|
self.networkBytesIn = network.bytesIn
|
|
self.networkBytesOut = network.bytesOut
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - CPU Monitoring
|
|
|
|
private func getCPULoadInfo() -> host_cpu_load_info? {
|
|
var cpuLoadInfo = host_cpu_load_info()
|
|
var count = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info>.stride / MemoryLayout<integer_t>.stride)
|
|
|
|
let result = withUnsafeMutablePointer(to: &cpuLoadInfo) {
|
|
$0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
|
|
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &count)
|
|
}
|
|
}
|
|
|
|
return result == KERN_SUCCESS ? cpuLoadInfo : nil
|
|
}
|
|
|
|
private func calculateCPUUsage() -> (total: Double, user: Double, system: Double) {
|
|
guard let currentInfo = getCPULoadInfo(),
|
|
let previousInfo = previousCPUInfo else {
|
|
return (0, 0, 0)
|
|
}
|
|
|
|
let userDiff = Double(currentInfo.cpu_ticks.0 - previousInfo.cpu_ticks.0)
|
|
let systemDiff = Double(currentInfo.cpu_ticks.1 - previousInfo.cpu_ticks.1)
|
|
let idleDiff = Double(currentInfo.cpu_ticks.2 - previousInfo.cpu_ticks.2)
|
|
let niceDiff = Double(currentInfo.cpu_ticks.3 - previousInfo.cpu_ticks.3)
|
|
|
|
let totalTicks = userDiff + systemDiff + idleDiff + niceDiff
|
|
|
|
guard totalTicks > 0 else { return (0, 0, 0) }
|
|
|
|
let userPercent = (userDiff / totalTicks) * 100
|
|
let systemPercent = (systemDiff / totalTicks) * 100
|
|
let totalPercent = ((userDiff + systemDiff + niceDiff) / totalTicks) * 100
|
|
|
|
previousCPUInfo = currentInfo
|
|
|
|
return (min(totalPercent, 100), min(userPercent, 100), min(systemPercent, 100))
|
|
}
|
|
|
|
// MARK: - Memory Monitoring
|
|
|
|
private func calculateMemoryUsage() -> (percentage: Double, used: UInt64, total: UInt64) {
|
|
var stats = vm_statistics64()
|
|
var count = mach_msg_type_number_t(MemoryLayout<vm_statistics64>.stride / MemoryLayout<integer_t>.stride)
|
|
|
|
let result = withUnsafeMutablePointer(to: &stats) {
|
|
$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 (0, 0, 0)
|
|
}
|
|
|
|
let pageSize = UInt64(vm_kernel_page_size)
|
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
|
|
// Calculate used memory
|
|
let activeMemory = UInt64(stats.active_count) * pageSize
|
|
let wiredMemory = UInt64(stats.wire_count) * pageSize
|
|
let compressedMemory = UInt64(stats.compressor_page_count) * pageSize
|
|
|
|
let usedMemory = activeMemory + wiredMemory + compressedMemory
|
|
let percentage = (Double(usedMemory) / Double(totalMemory)) * 100
|
|
|
|
return (min(percentage, 100), usedMemory, totalMemory)
|
|
}
|
|
|
|
// MARK: - Disk Monitoring
|
|
|
|
private func getDiskBytes() -> (read: UInt64, write: UInt64) {
|
|
// Use IOKit for disk statistics
|
|
// Simplified implementation - returns approximate values
|
|
var readBytes: UInt64 = 0
|
|
var writeBytes: UInt64 = 0
|
|
|
|
// Get disk statistics from system
|
|
let task = Process()
|
|
task.launchPath = "/usr/bin/iostat"
|
|
task.arguments = ["-d", "-c", "1"]
|
|
|
|
let pipe = Pipe()
|
|
task.standardOutput = pipe
|
|
|
|
do {
|
|
try task.run()
|
|
task.waitUntilExit()
|
|
|
|
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
if let output = String(data: data, encoding: .utf8) {
|
|
// Parse iostat output
|
|
let lines = output.components(separatedBy: "\n")
|
|
if lines.count > 2 {
|
|
let values = lines[2].split(separator: " ").compactMap { Double($0) }
|
|
if values.count >= 3 {
|
|
// KB/t, tps, MB/s
|
|
readBytes = UInt64(values.last ?? 0 * 1024 * 1024)
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Fallback to simulated values
|
|
}
|
|
|
|
return (readBytes, writeBytes)
|
|
}
|
|
|
|
private func calculateDiskActivity() -> Double {
|
|
let currentBytes = getDiskBytes()
|
|
let readDiff = currentBytes.read > previousDiskBytes.read ?
|
|
currentBytes.read - previousDiskBytes.read : 0
|
|
let writeDiff = currentBytes.write > previousDiskBytes.write ?
|
|
currentBytes.write - previousDiskBytes.write : 0
|
|
|
|
previousDiskBytes = currentBytes
|
|
|
|
// Normalize to percentage (assuming 100MB/s as max)
|
|
let totalBytes = Double(readDiff + writeDiff)
|
|
let maxBytesPerInterval = 100.0 * 1024 * 1024 * updateInterval
|
|
let percentage = (totalBytes / maxBytesPerInterval) * 100
|
|
|
|
return min(percentage, 100)
|
|
}
|
|
|
|
// MARK: - Network Monitoring
|
|
|
|
private func getNetworkBytes() -> (in: UInt64, out: UInt64) {
|
|
var ifaddr: UnsafeMutablePointer<ifaddrs>?
|
|
var bytesIn: UInt64 = 0
|
|
var bytesOut: UInt64 = 0
|
|
|
|
guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else {
|
|
return (0, 0)
|
|
}
|
|
|
|
defer { freeifaddrs(ifaddr) }
|
|
|
|
var ptr = firstAddr
|
|
while true {
|
|
let interface = ptr.pointee
|
|
|
|
// Check for data link layer
|
|
if interface.ifa_addr.pointee.sa_family == UInt8(AF_LINK) {
|
|
// Get network interface data
|
|
if let data = interface.ifa_data {
|
|
let networkData = data.assumingMemoryBound(to: if_data.self).pointee
|
|
bytesIn += UInt64(networkData.ifi_ibytes)
|
|
bytesOut += UInt64(networkData.ifi_obytes)
|
|
}
|
|
}
|
|
|
|
guard let next = interface.ifa_next else { break }
|
|
ptr = next
|
|
}
|
|
|
|
return (bytesIn, bytesOut)
|
|
}
|
|
|
|
private func calculateNetworkActivity() -> (percentage: Double, bytesIn: UInt64, bytesOut: UInt64) {
|
|
let currentBytes = getNetworkBytes()
|
|
|
|
let bytesInDiff = currentBytes.in > previousNetworkBytes.in ?
|
|
currentBytes.in - previousNetworkBytes.in : 0
|
|
let bytesOutDiff = currentBytes.out > previousNetworkBytes.out ?
|
|
currentBytes.out - previousNetworkBytes.out : 0
|
|
|
|
previousNetworkBytes = currentBytes
|
|
|
|
// Calculate rate in bytes per second
|
|
let totalBytesPerSecond = Double(bytesInDiff + bytesOutDiff) / updateInterval
|
|
|
|
// Normalize to percentage (assuming 100 Mbps as reference)
|
|
let maxBytesPerSecond = 100.0 * 1024 * 1024 / 8 // 100 Mbps in bytes
|
|
let percentage = (totalBytesPerSecond / maxBytesPerSecond) * 100
|
|
|
|
return (min(percentage, 100), bytesInDiff, bytesOutDiff)
|
|
}
|
|
}
|
|
|
|
// MARK: - Memory Formatter Extension
|
|
extension SystemMonitor {
|
|
/// Format bytes to human readable string
|
|
static func formatBytes(_ bytes: UInt64) -> String {
|
|
let formatter = ByteCountFormatter()
|
|
formatter.countStyle = .memory
|
|
return formatter.string(fromByteCount: Int64(bytes))
|
|
}
|
|
}
|