// // StatsManager.swift // DynamicIsland // // System performance monitoring for the Atoll Stats feature // Created by Hariharan Mudaliar import Foundation import Combine import Defaults import SwiftUI import IOKit import IOKit.ps import IOKit.graphics import Darwin import AppKit //import Network struct MemoryBreakdown: Equatable { let totalBytes: UInt64 let usedBytes: UInt64 let freeBytes: UInt64 let wiredBytes: UInt64 let activeBytes: UInt64 let inactiveBytes: UInt64 let compressedBytes: UInt64 let appBytes: UInt64 let cacheBytes: UInt64 let swap: MemorySwap let pressure: MemoryPressure static let zero = MemoryBreakdown( totalBytes: 0, usedBytes: 0, freeBytes: 0, wiredBytes: 0, activeBytes: 0, inactiveBytes: 0, compressedBytes: 0, appBytes: 0, cacheBytes: 0, swap: .zero, pressure: .unknown ) var usedPercentage: Double { guard totalBytes > 0 else { return 0 } return Double(usedBytes) / Double(totalBytes) * 100 } var freePercentage: Double { guard totalBytes > 0 else { return 0 } return Double(freeBytes) / Double(totalBytes) * 100 } var appPercentage: Double { guard totalBytes > 0 else { return 0 } return Double(appBytes) / Double(totalBytes) * 100 } var cachePercentage: Double { guard totalBytes > 0 else { return 0 } return Double(cacheBytes) / Double(totalBytes) * 100 } } struct MemorySwap: Equatable { let totalBytes: UInt64 let usedBytes: UInt64 let freeBytes: UInt64 static let zero = MemorySwap(totalBytes: 0, usedBytes: 0, freeBytes: 0) } enum MemoryPressureLevel: String, Equatable { case normal case warning case critical } struct MemoryPressure: Equatable { let rawValue: Int let level: MemoryPressureLevel static let unknown = MemoryPressure(rawValue: 0, level: .normal) } struct CPUCoreUsage: Identifiable, Equatable { let id: Int let usage: Double } // MARK: - GPU Helpers private final class GPUInfoCollector { func collectDevices() -> [GPUDeviceMetrics] { var devices: [GPUDeviceMetrics] = [] let matching = IOServiceMatching(kIOAcceleratorClassName) var iterator: io_iterator_t = 0 guard IOServiceGetMatchingServices(kIOMainPortDefault, matching, &iterator) == KERN_SUCCESS else { return devices } defer { IOObjectRelease(iterator) } var index = 0 var service = IOIteratorNext(iterator) while service != 0 { if let properties = copyProperties(for: service), let device = makeDevice(from: properties, index: index) { devices.append(device) } IOObjectRelease(service) service = IOIteratorNext(iterator) index += 1 } return devices } private func copyProperties(for service: io_registry_entry_t) -> [String: Any]? { var properties: Unmanaged? guard IORegistryEntryCreateCFProperties(service, &properties, kCFAllocatorDefault, 0) == KERN_SUCCESS, let dict = properties?.takeRetainedValue() as? [String: Any] else { return nil } return dict } private func makeDevice(from dict: [String: Any], index: Int) -> GPUDeviceMetrics? { guard let ioClass = dict["IOClass"] as? String else { return nil } let stats = dict["PerformanceStatistics"] as? [String: Any] ?? [:] let vendor = vendorName(from: ioClass) ?? (dict["vendor"] as? String) let model = sanitizedModel(primary: stats["model"] as? String, secondary: dict["model"] as? String, vendorFallback: vendor) let id = "\(model)#\(index + 1)" let utilization = percentValue(for: ["Device Utilization %", "GPU Activity(%)"], in: stats) let renderUtilization = percentValue(for: ["Renderer Utilization %"], in: stats) let tilerUtilization = percentValue(for: ["Tiler Utilization %"], in: stats) let temperature = numericValue(for: ["Temperature(C)", "temperature"], in: stats) let fanSpeed = intValue(for: ["Fan Speed(%)"], in: stats) let coreClock = intValue(for: ["Core Clock(MHz)"], in: stats) let memoryClock = intValue(for: ["Memory Clock(MHz)"], in: stats) let cores = (dict["gpu-core-count"] as? NSNumber)?.intValue ?? (dict["Cores"] as? Int) let isActive = isAcceleratorActive(from: dict) return GPUDeviceMetrics( id: id, vendor: vendor, model: model, isActive: isActive, utilization: utilization, renderUtilization: renderUtilization, tilerUtilization: tilerUtilization, temperature: temperature, fanSpeed: fanSpeed, coreClock: coreClock, memoryClock: memoryClock, cores: cores ) } private func vendorName(from ioClass: String) -> String? { let value = ioClass.lowercased() if value.contains("nvidia") { return "NVIDIA" } else if value.contains("amd") { return "AMD" } else if value.contains("intel") { return "Intel" } else if value.contains("agx") || value.contains("apple") { return "Apple" } return nil } private func sanitizedModel(primary: String?, secondary: String?, vendorFallback: String?) -> String { let normalizedPrimary = normalizedString(primary) if let normalizedPrimary, !normalizedPrimary.isEmpty { return normalizedPrimary } let normalizedSecondary = normalizedString(secondary) if let normalizedSecondary, !normalizedSecondary.isEmpty { return normalizedSecondary } if let vendorFallback, !vendorFallback.isEmpty { return "\(vendorFallback) Graphics" } return "GPU" } private func normalizedString(_ raw: String?) -> String? { guard var value = raw else { return nil } value = value.replacingOccurrences(of: "\0", with: "") value = value.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } private func percentValue(for keys: [String], in dict: [String: Any]) -> Double? { for key in keys { if let number = dict[key] as? NSNumber { return clampPercent(number.doubleValue) } if let value = dict[key] as? Double { return clampPercent(value) } if let value = dict[key] as? Int { return clampPercent(Double(value)) } } return nil } private func numericValue(for keys: [String], in dict: [String: Any]) -> Double? { for key in keys { if let number = dict[key] as? NSNumber { return number.doubleValue } if let value = dict[key] as? Double { return value } if let value = dict[key] as? Int { return Double(value) } } return nil } private func intValue(for keys: [String], in dict: [String: Any]) -> Int? { for key in keys { if let number = dict[key] as? NSNumber { return number.intValue } if let value = dict[key] as? Int { return value } } return nil } private func isAcceleratorActive(from dict: [String: Any]) -> Bool { guard let agcInfo = dict["AGCInfo"] as? [String: Any] else { return true } if let poweredOff = agcInfo["poweredOffByAGC"] as? NSNumber { return poweredOff.intValue == 0 } if let poweredOff = agcInfo["poweredOffByAGC"] as? Int { return poweredOff == 0 } return true } private func clampPercent(_ value: Double) -> Double { return min(max(value, 0), 100) } } struct GPUBreakdown: Equatable { let render: Double let compute: Double let video: Double let other: Double static let zero = GPUBreakdown(render: 0, compute: 0, video: 0, other: 0) var totalUsage: Double { render + compute + video + other } } struct GPUMetricsSnapshot { let usage: Double let breakdown: GPUBreakdown let devices: [GPUDeviceMetrics] static let zero = GPUMetricsSnapshot(usage: 0, breakdown: .zero, devices: []) } enum NetworkInterfaceType: String { case wifi case ethernet case loopback case cellular case other } struct NetworkInterfaceMetrics: Identifiable, Equatable { let name: String let displayName: String let type: NetworkInterfaceType let ipv4: String? let ipv6: String? let isActive: Bool let currentDownload: Double let currentUpload: Double let totalDownloaded: Double let totalUploaded: Double var id: String { name } } struct DiskDeviceMetrics: Identifiable, Equatable { let id: String let name: String let path: URL let totalBytes: UInt64 let freeBytes: UInt64 let isRoot: Bool let isRemovable: Bool var usedBytes: UInt64 { totalBytes > freeBytes ? totalBytes - freeBytes : 0 } var usagePercentage: Double { guard totalBytes > 0 else { return 0 } return Double(usedBytes) / Double(totalBytes) * 100 } } struct GPUDeviceMetrics: Identifiable, Equatable { let id: String let vendor: String? let model: String let isActive: Bool let utilization: Double? let renderUtilization: Double? let tilerUtilization: Double? let temperature: Double? let fanSpeed: Int? let coreClock: Int? let memoryClock: Int? let cores: Int? var formattedVendorModel: String { if let vendor { return vendor == model ? model : "\(vendor) \(model)".trimmingCharacters(in: .whitespaces) } return model } var utilizationText: String { guard let utilization else { return "—" } return StatsFormatting.percentage(utilization) } var temperatureText: String { guard let temperature else { return "—" } return String(format: "%.0f°C", temperature) } } struct NetworkTotals: Equatable { var downloadedMB: Double var uploadedMB: Double static let zero = NetworkTotals(downloadedMB: 0, uploadedMB: 0) } struct DiskTotals: Equatable { var readMB: Double var writtenMB: Double static let zero = DiskTotals(readMB: 0, writtenMB: 0) } class StatsManager: ObservableObject { // MARK: - Properties static let shared = StatsManager() @Published var isMonitoring: Bool = false @Published var cpuUsage: Double = 0.0 @Published var memoryUsage: Double = 0.0 @Published var gpuUsage: Double = 0.0 @Published var networkDownload: Double = 0.0 // MB/s @Published var networkUpload: Double = 0.0 // MB/s @Published var diskRead: Double = 0.0 // MB/s @Published var diskWrite: Double = 0.0 // MB/s @Published var lastUpdated: Date = .distantPast @Published private(set) var cpuBreakdown: CPULoadBreakdown = .zero @Published private(set) var cpuLoadAverage: LoadAverage = .zero @Published private(set) var cpuCoreUsage: [CPUCoreUsage] = [] @Published private(set) var cpuUptime: TimeInterval = 0 @Published private(set) var topCPUProcesses: [ProcessStats] = [] @Published private(set) var cpuTemperature: CPUTemperatureMetrics = CPUTemperatureMetrics(celsius: nil) @Published private(set) var cpuFrequency: CPUFrequencyMetrics? @Published private(set) var memoryBreakdown: MemoryBreakdown = .zero @Published private(set) var gpuBreakdown: GPUBreakdown = .zero @Published private(set) var gpuDevices: [GPUDeviceMetrics] = [] @Published private(set) var networkTotals: NetworkTotals = .zero @Published private(set) var diskTotals: DiskTotals = .zero @Published private(set) var networkInterfaces: [NetworkInterfaceMetrics] = [] @Published private(set) var diskDevices: [DiskDeviceMetrics] = [] // Historical data for graphs (last 30 data points) @Published var cpuHistory: [Double] = [] @Published var memoryHistory: [Double] = [] @Published var gpuHistory: [Double] = [] @Published var networkDownloadHistory: [Double] = [] @Published var networkUploadHistory: [Double] = [] @Published var diskReadHistory: [Double] = [] @Published var diskWriteHistory: [Double] = [] private var monitoringTimer: Timer? private var delayedStopTimer: Timer? private var delayedStartTimer: Timer? private let maxHistoryPoints = 30 private let totalPhysicalMemory: UInt64 = { var stats = host_basic_info() var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) let result = withUnsafeMutablePointer(to: &stats) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { host_info(mach_host_self(), HOST_BASIC_INFO, $0, &count) } } if result == KERN_SUCCESS { return UInt64(stats.max_mem) } return UInt64(ProcessInfo.processInfo.physicalMemory) }() // Smart monitoring state private var shouldMonitorForStats: Bool = false private var lastNotchState: String = "closed" private var lastCurrentView: String = "other" // Network monitoring state private var previousNetworkStats: (bytesIn: UInt64, bytesOut: UInt64) = (0, 0) private var previousTimestamp: Date = Date() // Disk monitoring state private var previousDiskStats: (bytesRead: UInt64, bytesWritten: UInt64) = (0, 0) private var previousCPULoadInfo: host_cpu_load_info? private var previousCpuInfo: processor_info_array_t? private var previousCpuInfoCount: mach_msg_type_number_t = 0 private var processorCount: natural_t = 0 private var previousInterfaceCounters: [String: (bytesIn: UInt64, bytesOut: UInt64)] = [:] private var interfaceTotals: [String: NetworkTotals] = [:] // Per-process monitoring cache (updated via ps sampling) private var cachedProcessStats: [ProcessStats] = [] private var lastProcessStatsUpdate: Date = .distantPast private let processStatsUpdateInterval: TimeInterval = 2.0 private let maxProcessEntries: Int = 20 private var isProcessRefreshInFlight = false private let gpuCollector = GPUInfoCollector() private let cpuSensorCollector = CPUSensorCollector() private var cancellables = Set() private let minUpdateInterval: TimeInterval = 1.0 private let maxUpdateInterval: TimeInterval = 60.0 private let notchCloseStopDelay: TimeInterval = 3.0 private let tabSwitchStopDelay: TimeInterval = 0.1 // MARK: - Initialization private init() { // Initialize with empty history cpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) memoryHistory = Array(repeating: 0.0, count: maxHistoryPoints) gpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) networkDownloadHistory = Array(repeating: 0.0, count: maxHistoryPoints) networkUploadHistory = Array(repeating: 0.0, count: maxHistoryPoints) diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints) diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints) // Initialize baseline network stats let initialStats = getNetworkStats() previousNetworkStats = initialStats previousTimestamp = Date() // Initialize baseline disk stats let initialDiskStats = getDiskStats() previousDiskStats = initialDiskStats Defaults.publisher(.statsUpdateInterval, options: []).sink { [weak self] change in self?.handleUpdateIntervalChange(change.newValue) }.store(in: &cancellables) Defaults.publisher(.statsStopWhenNotchCloses, options: []).sink { [weak self] change in guard let self else { return } if change.newValue { if self.isMonitoring && self.lastNotchState == "closed" { self.scheduleDelayedStop(after: self.notchCloseStopDelay) } } else { self.delayedStopTimer?.invalidate() self.delayedStopTimer = nil } }.store(in: &cancellables) } deinit { stopMonitoring() delayedStartTimer?.invalidate() delayedStopTimer?.invalidate() } // MARK: - Smart Monitoring func updateMonitoringState(notchIsOpen: Bool, currentView: String) { let notchState = notchIsOpen ? "open" : "closed" // Only react to actual state changes guard notchState != lastNotchState || currentView != lastCurrentView else { return } lastNotchState = notchState lastCurrentView = currentView // Cancel any pending timers delayedStartTimer?.invalidate() delayedStopTimer?.invalidate() // Determine if we should be monitoring shouldMonitorForStats = notchIsOpen && (currentView == "stats") if shouldMonitorForStats { // Start monitoring after 3.5 seconds (when notch is open and stats tab is active) delayedStartTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in DispatchQueue.main.async { self?.startMonitoring() } } } else { if notchIsOpen && currentView != "stats" { scheduleDelayedStop(after: tabSwitchStopDelay) } else if !notchIsOpen { if Defaults[.statsStopWhenNotchCloses] { scheduleDelayedStop(after: notchCloseStopDelay) } } else { scheduleDelayedStop(after: tabSwitchStopDelay) } } } // MARK: - Public Monitoring Controls func startMonitoring() { guard !isMonitoring else { return } print("StatsManager: Starting monitoring...") // Reset baseline for accurate measurement let initialStats = getNetworkStats() previousNetworkStats = initialStats let initialDiskStats = getDiskStats() previousDiskStats = initialDiskStats previousTimestamp = Date() isMonitoring = true lastUpdated = Date() networkTotals = .zero diskTotals = .zero scheduleMonitoringTimer() Task { @MainActor in self.updateSystemStats() } print("StatsManager: Monitoring started") } func stopMonitoring() { guard isMonitoring else { return } // Clean up all timers monitoringTimer?.invalidate() monitoringTimer = nil delayedStartTimer?.invalidate() delayedStopTimer?.invalidate() isMonitoring = false print("StatsManager: Monitoring stopped") cachedProcessStats.removeAll() lastProcessStatsUpdate = .distantPast isProcessRefreshInFlight = false previousCPULoadInfo = nil if let previousCpuInfo { let size = vm_size_t(previousCpuInfoCount) * vm_size_t(MemoryLayout.size) vm_deallocate(mach_task_self_, vm_address_t(bitPattern: previousCpuInfo), size) } previousCpuInfo = nil previousCpuInfoCount = 0 processorCount = 0 topCPUProcesses = [] cpuCoreUsage = [] networkInterfaces = [] diskDevices = [] previousInterfaceCounters.removeAll() interfaceTotals.removeAll() cpuTemperature = CPUTemperatureMetrics(celsius: nil) cpuFrequency = nil } private func scheduleMonitoringTimer() { monitoringTimer?.invalidate() let configuredInterval = Defaults[.statsUpdateInterval] let interval = validatedUpdateInterval(configuredInterval) if abs(interval - configuredInterval) > 0.0001 { Defaults[.statsUpdateInterval] = interval } monitoringTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in guard let self = self else { return } Task { @MainActor in self.updateSystemStats() } } } private func handleUpdateIntervalChange(_ newValue: Double) { let interval = validatedUpdateInterval(newValue) if abs(interval - newValue) > 0.0001 { Defaults[.statsUpdateInterval] = interval return } guard isMonitoring else { return } DispatchQueue.main.async { [weak self] in self?.scheduleMonitoringTimer() } } private func validatedUpdateInterval(_ value: Double) -> TimeInterval { min(max(value, minUpdateInterval), maxUpdateInterval) } private func scheduleDelayedStop(after delay: TimeInterval) { guard delay > 0 else { stopMonitoring() return } delayedStopTimer?.invalidate() delayedStopTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in DispatchQueue.main.async { self?.stopMonitoring() } } } // MARK: - Private Methods @MainActor private func updateSystemStats() { let cpuMetrics = getCPULoadBreakdown() let newCpuUsage = cpuMetrics.activeUsage let memorySnapshot = getMemorySnapshot() let newMemoryUsage = memorySnapshot.usage let gpuSnapshot = getGPUMetrics() let newGpuUsage = gpuSnapshot.usage let coreUsage = collectCPUCoreUsage() // Calculate network speeds let currentNetworkStats = getNetworkStats() let currentTime = Date() let timeInterval = currentTime.timeIntervalSince(previousTimestamp) var downloadSpeed: Double = 0.0 var uploadSpeed: Double = 0.0 var bytesDownloaded: UInt64 = 0 var bytesUploaded: UInt64 = 0 // Only calculate speeds if we have a reasonable time interval and this isn't the first run if timeInterval > 0.1 && (previousNetworkStats.bytesIn > 0 || previousNetworkStats.bytesOut > 0) { bytesDownloaded = currentNetworkStats.bytesIn > previousNetworkStats.bytesIn ? currentNetworkStats.bytesIn - previousNetworkStats.bytesIn : 0 bytesUploaded = currentNetworkStats.bytesOut > previousNetworkStats.bytesOut ? currentNetworkStats.bytesOut - previousNetworkStats.bytesOut : 0 downloadSpeed = Double(bytesDownloaded) / timeInterval / 1_048_576 // Convert to MB/s uploadSpeed = Double(bytesUploaded) / timeInterval / 1_048_576 // Convert to MB/s } // Calculate disk speeds let currentDiskStats = getDiskStats() var readSpeed: Double = 0.0 var writeSpeed: Double = 0.0 var bytesRead: UInt64 = 0 var bytesWritten: UInt64 = 0 // Only calculate speeds if we have a reasonable time interval and this isn't the first run if timeInterval > 0.1 && (previousDiskStats.bytesRead > 0 || previousDiskStats.bytesWritten > 0) { bytesRead = currentDiskStats.bytesRead > previousDiskStats.bytesRead ? currentDiskStats.bytesRead - previousDiskStats.bytesRead : 0 bytesWritten = currentDiskStats.bytesWritten > previousDiskStats.bytesWritten ? currentDiskStats.bytesWritten - previousDiskStats.bytesWritten : 0 readSpeed = Double(bytesRead) / timeInterval / 1_048_576 // Convert to MB/s writeSpeed = Double(bytesWritten) / timeInterval / 1_048_576 // Convert to MB/s } // Update cumulative transfer totals if bytesDownloaded > 0 { var updatedTotals = networkTotals updatedTotals.downloadedMB += Double(bytesDownloaded) / 1_048_576 networkTotals = updatedTotals } if bytesUploaded > 0 { var updatedTotals = networkTotals updatedTotals.uploadedMB += Double(bytesUploaded) / 1_048_576 networkTotals = updatedTotals } if bytesRead > 0 { var updatedDiskTotals = diskTotals updatedDiskTotals.readMB += Double(bytesRead) / 1_048_576 diskTotals = updatedDiskTotals } if bytesWritten > 0 { var updatedDiskTotals = diskTotals updatedDiskTotals.writtenMB += Double(bytesWritten) / 1_048_576 diskTotals = updatedDiskTotals } // Update current values cpuUsage = newCpuUsage gpuUsage = newGpuUsage memoryUsage = newMemoryUsage networkDownload = max(0.0, downloadSpeed) networkUpload = max(0.0, uploadSpeed) diskRead = max(0.0, readSpeed) diskWrite = max(0.0, writeSpeed) lastUpdated = Date() cpuBreakdown = cpuMetrics memoryBreakdown = memorySnapshot.breakdown cpuLoadAverage = getLoadAverage() gpuBreakdown = gpuSnapshot.breakdown if gpuDevices != gpuSnapshot.devices { gpuDevices = gpuSnapshot.devices } if cpuCoreUsage != coreUsage { cpuCoreUsage = coreUsage } cpuUptime = ProcessInfo.processInfo.systemUptime cpuTemperature = cpuSensorCollector.readTemperature() if let frequencyMetrics = cpuSensorCollector.readFrequency() { cpuFrequency = frequencyMetrics } // Update history arrays (sliding window) updateHistory(value: newCpuUsage, history: &cpuHistory) updateHistory(value: newMemoryUsage, history: &memoryHistory) updateHistory(value: newGpuUsage, history: &gpuHistory) updateHistory(value: downloadSpeed, history: &networkDownloadHistory) updateHistory(value: uploadSpeed, history: &networkUploadHistory) updateHistory(value: readSpeed, history: &diskReadHistory) updateHistory(value: writeSpeed, history: &diskWriteHistory) // Update previous stats for next calculation previousNetworkStats = currentNetworkStats previousDiskStats = currentDiskStats previousTimestamp = currentTime networkInterfaces = collectNetworkInterfaces(deltaTime: timeInterval) diskDevices = collectDiskDevices() refreshProcessStatsIfNeeded(force: true) } private func updateHistory(value: Double, history: inout [Double]) { // Remove first element and append new value if history.count >= maxHistoryPoints { history.removeFirst() } history.append(value) } // MARK: - System Monitoring Functions private func getCPULoadBreakdown() -> CPULoadBreakdown { var info = host_cpu_load_info() var count = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) let result = withUnsafeMutablePointer(to: &info) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &count) } } guard result == KERN_SUCCESS else { return cpuBreakdown } let clamped: (Double) -> Double = { value in return min(max(value, 0), 100) } let breakdown: CPULoadBreakdown if let previous = previousCPULoadInfo { let userDiff = Double(info.cpu_ticks.0 - previous.cpu_ticks.0) let systemDiff = Double(info.cpu_ticks.1 - previous.cpu_ticks.1) let idleDiff = Double(info.cpu_ticks.2 - previous.cpu_ticks.2) let niceDiff = Double(info.cpu_ticks.3 - previous.cpu_ticks.3) let total = userDiff + systemDiff + idleDiff + niceDiff if total > 0 { let userPercent = ((userDiff + niceDiff) / total) * 100 let systemPercent = (systemDiff / total) * 100 let idlePercent = (idleDiff / total) * 100 breakdown = CPULoadBreakdown( user: clamped(userPercent), system: clamped(systemPercent), idle: clamped(idlePercent) ) } else { breakdown = cpuBreakdown } } else { let totalTicks = Double(info.cpu_ticks.0 + info.cpu_ticks.1 + info.cpu_ticks.2 + info.cpu_ticks.3) if totalTicks > 0 { let userPercent = ((Double(info.cpu_ticks.0) + Double(info.cpu_ticks.3)) / totalTicks) * 100 let systemPercent = (Double(info.cpu_ticks.1) / totalTicks) * 100 let idlePercent = (Double(info.cpu_ticks.2) / totalTicks) * 100 breakdown = CPULoadBreakdown( user: clamped(userPercent), system: clamped(systemPercent), idle: clamped(idlePercent) ) } else { breakdown = cpuBreakdown } } previousCPULoadInfo = info return breakdown } private func getLoadAverage() -> LoadAverage { var loads = [Double](repeating: 0, count: 3) let result = loads.withUnsafeMutableBufferPointer { buffer -> Int32 in guard let baseAddress = buffer.baseAddress else { return 0 } return getloadavg(baseAddress, 3) } guard result == 3 else { return cpuLoadAverage } return LoadAverage(oneMinute: loads[0], fiveMinutes: loads[1], fifteenMinutes: loads[2]) } private func getMemorySnapshot() -> (usage: Double, breakdown: MemoryBreakdown) { // Memory usage monitoring using host_statistics64 var vmStatistics = vm_statistics64() var size = mach_msg_type_number_t(MemoryLayout.size / MemoryLayout.size) let vmResult = withUnsafeMutablePointer(to: &vmStatistics) { $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { host_statistics64(mach_host_self(), HOST_VM_INFO64, $0, &size) } } guard vmResult == KERN_SUCCESS else { return (memoryUsage, memoryBreakdown) } let pageSize = UInt64(vm_kernel_page_size) let freeBytes = UInt64(vmStatistics.free_count) * pageSize let speculativeBytes = UInt64(vmStatistics.speculative_count) * pageSize let activeBytes = UInt64(vmStatistics.active_count) * pageSize let inactiveBytes = UInt64(vmStatistics.inactive_count) * pageSize let wiredBytes = UInt64(vmStatistics.wire_count) * pageSize let compressedBytes = UInt64(vmStatistics.compressor_page_count) * pageSize let purgeableBytes = UInt64(vmStatistics.purgeable_count) * pageSize let externalBytes = UInt64(vmStatistics.external_page_count) * pageSize let totalMemoryBytes: UInt64 if totalPhysicalMemory > 0 { totalMemoryBytes = totalPhysicalMemory } else { totalMemoryBytes = freeBytes + speculativeBytes + activeBytes + inactiveBytes + wiredBytes + compressedBytes } guard totalMemoryBytes > 0 else { return (0.0, .zero) } let usedWithoutCache = activeBytes + inactiveBytes + speculativeBytes + wiredBytes + compressedBytes let cacheBytes = purgeableBytes + externalBytes let usedBytes = usedWithoutCache > cacheBytes ? usedWithoutCache - cacheBytes : 0 let clampedUsedBytes = min(usedBytes, totalMemoryBytes) let freeComputedBytes = totalMemoryBytes > clampedUsedBytes ? totalMemoryBytes - clampedUsedBytes : 0 let nonAppBytes = wiredBytes + compressedBytes let appBytes = clampedUsedBytes > nonAppBytes ? clampedUsedBytes - nonAppBytes : 0 var pressureLevelRaw: Int32 = 0 var pressureSize = MemoryLayout.size sysctlbyname("kern.memorystatus_vm_pressure_level", &pressureLevelRaw, &pressureSize, nil, 0) let pressureLevel: MemoryPressureLevel switch pressureLevelRaw { case 2: pressureLevel = .warning case 4: pressureLevel = .critical default: pressureLevel = .normal } let pressure = MemoryPressure(rawValue: Int(pressureLevelRaw), level: pressureLevel) var swapUsage = xsw_usage() var swapSize = MemoryLayout.size sysctlbyname("vm.swapusage", &swapUsage, &swapSize, nil, 0) let swapInfo = MemorySwap( totalBytes: UInt64(swapUsage.xsu_total), usedBytes: UInt64(swapUsage.xsu_used), freeBytes: UInt64(swapUsage.xsu_avail) ) let usage = Double(clampedUsedBytes) / Double(totalMemoryBytes) * 100.0 let breakdown = MemoryBreakdown( totalBytes: totalMemoryBytes, usedBytes: clampedUsedBytes, freeBytes: freeComputedBytes, wiredBytes: wiredBytes, activeBytes: activeBytes, inactiveBytes: inactiveBytes, compressedBytes: compressedBytes, appBytes: appBytes, cacheBytes: cacheBytes, swap: swapInfo, pressure: pressure ) return (min(100.0, max(0.0, usage)), breakdown) } private func getGPUMetrics() -> GPUMetricsSnapshot { let devices = gpuCollector.collectDevices() guard !devices.isEmpty else { return .zero } let utilizationValues = devices.compactMap { $0.utilization } let usage: Double if utilizationValues.isEmpty { usage = 0 } else { usage = utilizationValues.reduce(0, +) / Double(utilizationValues.count) } let breakdown = makeGPUBreakdown(from: devices) return GPUMetricsSnapshot(usage: usage, breakdown: breakdown, devices: devices) } private func makeGPUBreakdown(from devices: [GPUDeviceMetrics]) -> GPUBreakdown { guard let primary = devices.first(where: { ($0.utilization ?? 0) > 0 || ($0.renderUtilization ?? 0) > 0 || ($0.tilerUtilization ?? 0) > 0 }) else { return .zero } let fallbackTotal = max((primary.renderUtilization ?? 0) + (primary.tilerUtilization ?? 0), 0) let total = max(primary.utilization ?? fallbackTotal, 0) if total.isZero { return GPUBreakdown( render: primary.renderUtilization ?? 0, compute: primary.tilerUtilization ?? 0, video: 0, other: 0 ) } let render = min(primary.renderUtilization ?? 0, total) let tiler = min(primary.tilerUtilization ?? 0, max(total - render, 0)) let remaining = max(total - render - tiler, 0) let compute = remaining * 0.6 let video = remaining * 0.25 let other = max(remaining - compute - video, 0) return GPUBreakdown(render: render, compute: compute, video: video, other: other) } private func collectCPUCoreUsage() -> [CPUCoreUsage] { var cpuInfo: processor_info_array_t? var numCpuInfo: mach_msg_type_number_t = 0 var numCPUsU: natural_t = 0 let result = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuInfo, &numCpuInfo) guard result == KERN_SUCCESS, let cpuInfo else { return cpuCoreUsage } let cpuCount = Int(numCPUsU) var usages: [CPUCoreUsage] = [] usages.reserveCapacity(cpuCount) let cpuStateMax = Int(CPU_STATE_MAX) if let previousCpuInfo { for cpu in 0.. 0 ? Double(inUse) / Double(total) : 0 usages.append(CPUCoreUsage(id: cpu, usage: max(0, min(percent * 100, 100)))) } } else { for cpu in 0.. 0 ? Double(inUse) / Double(total) : 0 usages.append(CPUCoreUsage(id: cpu, usage: max(0, min(percent * 100, 100)))) } } if let previousCpuInfo { let size = vm_size_t(previousCpuInfoCount) * vm_size_t(MemoryLayout.size) vm_deallocate(mach_task_self_, vm_address_t(bitPattern: previousCpuInfo), size) } previousCpuInfo = cpuInfo previousCpuInfoCount = numCpuInfo processorCount = numCPUsU return usages } private func collectNetworkInterfaces(deltaTime: TimeInterval) -> [NetworkInterfaceMetrics] { var interfacesPointer: UnsafeMutablePointer? = nil guard getifaddrs(&interfacesPointer) == 0, let startPointer = interfacesPointer else { return networkInterfaces } defer { freeifaddrs(startPointer) } struct InterfaceAccumulator { var name: String var flags: UInt32 var bytesIn: UInt64 var bytesOut: UInt64 var ipv4: String? var ipv6: String? } var accumulators: [String: InterfaceAccumulator] = [:] var pointer: UnsafeMutablePointer? = startPointer while let current = pointer { let interface = current.pointee let name = String(cString: interface.ifa_name) var accumulator = accumulators[name] ?? InterfaceAccumulator(name: name, flags: interface.ifa_flags, bytesIn: 0, bytesOut: 0, ipv4: nil, ipv6: nil) if let addr = interface.ifa_addr { switch Int32(addr.pointee.sa_family) { case AF_LINK: if let data = interface.ifa_data?.assumingMemoryBound(to: if_data.self) { accumulator.bytesIn = UInt64(data.pointee.ifi_ibytes) accumulator.bytesOut = UInt64(data.pointee.ifi_obytes) } case AF_INET: accumulator.ipv4 = stringFromSockaddr(addr) case AF_INET6: accumulator.ipv6 = stringFromSockaddr(addr) default: break } } accumulators[name] = accumulator pointer = interface.ifa_next } var results: [NetworkInterfaceMetrics] = [] var updatedCounters: [String: (bytesIn: UInt64, bytesOut: UInt64)] = [:] for accumulator in accumulators.values { guard shouldIncludeInterface(name: accumulator.name) else { continue } let previous = previousInterfaceCounters[accumulator.name] ?? (accumulator.bytesIn, accumulator.bytesOut) let deltaIn = accumulator.bytesIn >= previous.bytesIn ? accumulator.bytesIn - previous.bytesIn : 0 let deltaOut = accumulator.bytesOut >= previous.bytesOut ? accumulator.bytesOut - previous.bytesOut : 0 let speedDivider = max(deltaTime, 0.001) let downloadSpeed = Double(deltaIn) / speedDivider / 1_048_576 let uploadSpeed = Double(deltaOut) / speedDivider / 1_048_576 var totals = interfaceTotals[accumulator.name] ?? .zero totals.downloadedMB += Double(deltaIn) / 1_048_576 totals.uploadedMB += Double(deltaOut) / 1_048_576 interfaceTotals[accumulator.name] = totals let metrics = NetworkInterfaceMetrics( name: accumulator.name, displayName: interfaceDisplayName(for: accumulator.name), type: interfaceType(for: accumulator.name), ipv4: accumulator.ipv4, ipv6: accumulator.ipv6, isActive: interfaceIsActive(flags: accumulator.flags), currentDownload: max(downloadSpeed, 0), currentUpload: max(uploadSpeed, 0), totalDownloaded: totals.downloadedMB, totalUploaded: totals.uploadedMB ) results.append(metrics) updatedCounters[accumulator.name] = (accumulator.bytesIn, accumulator.bytesOut) } previousInterfaceCounters = updatedCounters return results.sorted { lhs, rhs in if lhs.type == rhs.type { return lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName) == .orderedAscending } return networkTypeSortOrder(lhs.type) < networkTypeSortOrder(rhs.type) } } private func collectDiskDevices() -> [DiskDeviceMetrics] { let keys: [URLResourceKey] = [ .volumeNameKey, .volumeTotalCapacityKey, .volumeAvailableCapacityKey, .volumeAvailableCapacityForImportantUsageKey, .volumeAvailableCapacityForOpportunisticUsageKey, .volumeIsRootFileSystemKey, .volumeIsRemovableKey ] guard let urls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: keys, options: [.skipHiddenVolumes]) else { return diskDevices } var devices: [DiskDeviceMetrics] = [] for url in urls { guard let values = try? url.resourceValues(forKeys: Set(keys)) else { continue } guard let totalCapacity = values.volumeTotalCapacity.flatMap({ Int64($0) }), totalCapacity > 0 else { continue } let name = values.volumeName ?? url.lastPathComponent let freeCapacityValue: Int64 if let free = values.volumeAvailableCapacity { freeCapacityValue = Int64(free) } else if let important = values.volumeAvailableCapacityForImportantUsage { freeCapacityValue = important } else if let opportunistic = values.volumeAvailableCapacityForOpportunisticUsage { freeCapacityValue = opportunistic } else { freeCapacityValue = 0 } let clampedFree = max(freeCapacityValue, 0) let device = DiskDeviceMetrics( id: url.path, name: name, path: url, totalBytes: UInt64(totalCapacity), freeBytes: UInt64(clampedFree), isRoot: values.volumeIsRootFileSystem ?? false, isRemovable: values.volumeIsRemovable ?? false ) devices.append(device) } return devices.sorted { lhs, rhs in if lhs.isRoot != rhs.isRoot { return lhs.isRoot } if lhs.isRemovable != rhs.isRemovable { return !lhs.isRemovable } return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending } } private func stringFromSockaddr(_ addressPointer: UnsafePointer?) -> String? { guard let addressPointer else { return nil } let family = Int32(addressPointer.pointee.sa_family) switch family { case AF_INET: var addr = addressPointer.withMemoryRebound(to: sockaddr_in.self, capacity: 1) { $0.pointee.sin_addr } var buffer = [CChar](repeating: 0, count: Int(INET_ADDRSTRLEN)) guard inet_ntop(AF_INET, &addr, &buffer, socklen_t(INET_ADDRSTRLEN)) != nil else { return nil } return String(cString: buffer) case AF_INET6: var addr = addressPointer.withMemoryRebound(to: sockaddr_in6.self, capacity: 1) { $0.pointee.sin6_addr } var buffer = [CChar](repeating: 0, count: Int(INET6_ADDRSTRLEN)) guard inet_ntop(AF_INET6, &addr, &buffer, socklen_t(INET6_ADDRSTRLEN)) != nil else { return nil } return String(cString: buffer) default: return nil } } private func shouldIncludeInterface(name: String) -> Bool { guard !name.hasPrefix("lo"), !name.hasPrefix("gif"), !name.hasPrefix("stf"), !name.hasPrefix("awdl"), !name.hasPrefix("llw"), !name.hasPrefix("utun"), !name.hasPrefix("p2p") else { return false } return true } private func interfaceIsActive(flags: UInt32) -> Bool { let isUp = (flags & UInt32(IFF_UP)) == UInt32(IFF_UP) let isRunning = (flags & UInt32(IFF_RUNNING)) == UInt32(IFF_RUNNING) return isUp && isRunning } private func interfaceDisplayName(for name: String) -> String { if name.hasPrefix("en") { return name == "en0" ? "Wi-Fi" : "Ethernet" } if name.hasPrefix("bridge") { return "Bridge" } if name.hasPrefix("pd") { return "USB" } if name.hasPrefix("vmnet") { return "Virtual" } if name.hasPrefix("ap") { return "Hotspot" } if name.hasPrefix("awdl") { return "AirDrop" } return name.uppercased() } private func interfaceType(for name: String) -> NetworkInterfaceType { if name.hasPrefix("en") { if name == "en0" { return .wifi } return .ethernet } if name.hasPrefix("bridge") || name.hasPrefix("vmnet") { return .ethernet } if name.hasPrefix("ap") { return .wifi } if name.hasPrefix("pdp_ip") { return .cellular } if name.hasPrefix("lo") { return .loopback } return .other } private func networkTypeSortOrder(_ type: NetworkInterfaceType) -> Int { switch type { case .wifi: return 0 case .ethernet: return 1 case .cellular: return 2 case .other: return 3 case .loopback: return 4 } } private func getNetworkStats() -> (bytesIn: UInt64, bytesOut: UInt64) { // Use BSD sockets to get network interface statistics var totalBytesIn: UInt64 = 0 var totalBytesOut: UInt64 = 0 var ifaddrs: UnsafeMutablePointer? guard getifaddrs(&ifaddrs) == 0 else { return (totalBytesIn, totalBytesOut) } defer { freeifaddrs(ifaddrs) } var ptr = ifaddrs while ptr != nil { defer { ptr = ptr?.pointee.ifa_next } guard let interface = ptr?.pointee, interface.ifa_addr.pointee.sa_family == UInt8(AF_LINK) else { continue } let name = String(cString: interface.ifa_name) // Skip loopback and virtual interfaces, but include en0, en1, etc. and Wi-Fi interfaces guard !name.hasPrefix("lo") && !name.hasPrefix("gif") && !name.hasPrefix("stf") && !name.hasPrefix("bridge") && !name.hasPrefix("utun") && !name.hasPrefix("awdl") else { continue } // Only count active interfaces (en0, en1, etc.) if name.hasPrefix("en") || name.contains("Wi-Fi") { if let data = interface.ifa_data?.assumingMemoryBound(to: if_data.self) { totalBytesIn += UInt64(data.pointee.ifi_ibytes) totalBytesOut += UInt64(data.pointee.ifi_obytes) } } } return (totalBytesIn, totalBytesOut) } private func getDiskStats() -> (bytesRead: UInt64, bytesWritten: UInt64) { // Use IOKit to get disk I/O statistics from IOStorage service var totalBytesRead: UInt64 = 0 var totalBytesWritten: UInt64 = 0 let matchingDict = IOServiceMatching("IOStorage") var iterator: io_iterator_t = 0 let result = IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) guard result == KERN_SUCCESS else { return (totalBytesRead, totalBytesWritten) } defer { IOObjectRelease(iterator) } var service: io_registry_entry_t = IOIteratorNext(iterator) while service != 0 { defer { IOObjectRelease(service) service = IOIteratorNext(iterator) } var properties: Unmanaged? let propertiesResult = IORegistryEntryCreateCFProperties(service, &properties, kCFAllocatorDefault, 0) guard propertiesResult == KERN_SUCCESS, let props = properties?.takeRetainedValue() as? [String: Any], let statistics = props["Statistics"] as? [String: Any] else { continue } // Use the correct property names for APFS/modern filesystems if let bytesRead = statistics["Bytes read from block device"] as? UInt64 { totalBytesRead += bytesRead } if let bytesWritten = statistics["Bytes written to block device"] as? UInt64 { totalBytesWritten += bytesWritten } } return (totalBytesRead, totalBytesWritten) } // MARK: - Computed Properties for UI var cpuUsageString: String { return String(format: "%.1f%%", cpuUsage) } var memoryUsageString: String { return String(format: "%.1f%%", memoryUsage) } var gpuUsageString: String { return String(format: "%.1f%%", gpuUsage) } var networkDownloadString: String { StatsFormatting.throughput(networkDownload) } var networkUploadString: String { StatsFormatting.throughput(networkUpload) } var diskReadString: String { return String(format: "%.1f MB/s", diskRead) } var diskWriteString: String { return String(format: "%.1f MB/s", diskWrite) } var maxCpuUsage: Double { return cpuHistory.max() ?? 0.0 } var maxMemoryUsage: Double { return memoryHistory.max() ?? 0.0 } var maxGpuUsage: Double { return gpuHistory.max() ?? 0.0 } var avgCpuUsage: Double { let nonZeroValues = cpuHistory.filter { $0 > 0 } guard !nonZeroValues.isEmpty else { return 0.0 } return nonZeroValues.reduce(0, +) / Double(nonZeroValues.count) } var avgMemoryUsage: Double { let nonZeroValues = memoryHistory.filter { $0 > 0 } guard !nonZeroValues.isEmpty else { return 0.0 } return nonZeroValues.reduce(0, +) / Double(nonZeroValues.count) } var avgGpuUsage: Double { let nonZeroValues = gpuHistory.filter { $0 > 0 } guard !nonZeroValues.isEmpty else { return 0.0 } return nonZeroValues.reduce(0, +) / Double(nonZeroValues.count) } // MARK: - Clear History Method func clearHistory() { cpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) memoryHistory = Array(repeating: 0.0, count: maxHistoryPoints) gpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) networkDownloadHistory = Array(repeating: 0.0, count: maxHistoryPoints) networkUploadHistory = Array(repeating: 0.0, count: maxHistoryPoints) diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints) diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints) } // MARK: - Process Monitoring Methods @MainActor func getProcessesRankedByCPU() -> [ProcessStats] { refreshProcessStatsIfNeeded() return cachedProcessStats.sorted { $0.cpuUsage > $1.cpuUsage } } @MainActor func getProcessesRankedByMemory() -> [ProcessStats] { refreshProcessStatsIfNeeded() return cachedProcessStats.sorted { $0.memoryUsage > $1.memoryUsage } } @MainActor func getProcessesRankedByGPU() -> [ProcessStats] { // For now, rank by CPU usage as GPU per-process stats are complex to obtain refreshProcessStatsIfNeeded() return cachedProcessStats.sorted { $0.cpuUsage > $1.cpuUsage } } @MainActor private func refreshProcessStatsIfNeeded(force: Bool = false) { let now = Date() guard force || now.timeIntervalSince(lastProcessStatsUpdate) >= processStatsUpdateInterval else { return } guard !isProcessRefreshInFlight else { return } isProcessRefreshInFlight = true Task.detached { [weak self] in guard let self else { return } let processes = StatsManager.collectTopProcesses(limit: self.maxProcessEntries) await MainActor.run { self.cachedProcessStats = processes self.lastProcessStatsUpdate = Date() self.isProcessRefreshInFlight = false self.topCPUProcesses = processes } } } private static func collectTopProcesses(limit: Int) -> [ProcessStats] { guard limit > 0 else { return [] } let task = Process() task.launchPath = "/bin/ps" task.arguments = ["-Aceo", "pid,pcpu,comm", "-r"] let outputPipe = Pipe() let errorPipe = Pipe() task.standardOutput = outputPipe task.standardError = errorPipe defer { outputPipe.fileHandleForReading.closeFile() errorPipe.fileHandleForReading.closeFile() } do { try task.run() } catch { NSLog("StatsManager: Failed to run ps command: \(error.localizedDescription)") return [] } let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() _ = errorPipe.fileHandleForReading.readDataToEndOfFile() guard !outputData.isEmpty, let output = String(data: outputData, encoding: .utf8) else { return [] } var results: [ProcessStats] = [] let lines = output.split(separator: "\n", omittingEmptySubsequences: true) guard lines.count > 1 else { return [] } for line in lines.dropFirst() { guard let parsed = parseProcessLine(String(line)) else { continue } let (pid, cpuUsage, command) = parsed let (displayName, icon) = runningApplicationInfo(for: pid, fallbackCommand: command) let memoryUsage = residentMemory(for: pid) let process = ProcessStats( pid: pid, name: displayName, cpuUsage: cpuUsage, memoryUsage: memoryUsage, icon: icon ) results.append(process) if results.count >= limit { break } } return results } private static func parseProcessLine(_ rawLine: String) -> (pid_t, Double, String)? { let trimmed = rawLine.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { return nil } let scanner = Scanner(string: trimmed) scanner.charactersToBeSkipped = .whitespaces guard let pidToken = scanner.scanCharacters(from: .decimalDigits), let pidValue = Int32(pidToken) else { return nil } let cpuCharacterSet = CharacterSet(charactersIn: "0123456789.,") guard let cpuToken = scanner.scanCharacters(from: cpuCharacterSet) else { return nil } let normalizedCPU = cpuToken.replacingOccurrences(of: ",", with: ".") guard let cpuValue = Double(normalizedCPU), cpuValue.isFinite, cpuValue >= 0 else { return nil } _ = scanner.scanCharacters(from: .whitespaces) let remainingIndex = scanner.currentIndex let command = String(trimmed[remainingIndex...]).trimmingCharacters(in: .whitespaces) return (pidValue, cpuValue, command.isEmpty ? "Unknown" : command) } private static func runningApplicationInfo(for pid: pid_t, fallbackCommand: String) -> (String, NSImage?) { if let app = NSRunningApplication(processIdentifier: pid) { let name = app.localizedName?.trimmingCharacters(in: .whitespacesAndNewlines) return ((name?.isEmpty ?? true) ? fallbackCommand : name!, app.icon) } return (fallbackCommand, nil) } private static func residentMemory(for pid: pid_t) -> UInt64 { var taskInfo = proc_taskinfo() let result = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, Int32(MemoryLayout.size)) guard result == MemoryLayout.size else { return 0 } return taskInfo.pti_resident_size } }