Files
puter/Sources/MacTaskManager/SystemMonitor.swift
T

944 lines
41 KiB
Swift

import Foundation
import Observation
@MainActor
@Observable
final class SystemMonitor {
var processes: [ProcessRecord] = []
var services: [ServiceRecord] = []
var appHistory: [AppUsageRecord] = []
var appHistoryStartDate = Date()
var processActivity: [Int32: ProcessActivityRate] = [:]
var snapshot = SystemSnapshot()
var cpuHistory: [MetricSample] = []
var coreHistories: [[MetricSample]] = []
var memoryHistory: [MetricSample] = []
var diskReadHistory: [MetricSample] = []
var diskWriteHistory: [MetricSample] = []
var diskLatencyHistory: [MetricSample] = []
var diskActiveHistory: [MetricSample] = []
var gpuHistory: [MetricSample] = []
var gpuRendererHistory: [MetricSample] = []
var gpuTilerHistory: [MetricSample] = []
var networkReceiveHistory: [MetricSample] = []
var networkSendHistory: [MetricSample] = []
var updateSpeed: UpdateSpeed {
didSet { UserDefaults.standard.set(updateSpeed.rawValue, forKey: "updateSpeed") }
}
var lastUpdated: Date?
var errorMessage: String?
private var updateTask: Task<Void, Never>?
private var previousCoreTicks: [CoreTicks] = []
private var previousNetworkCounters: NetworkCounters?
private var previousNetworkDate: Date?
private var previousDiskCounters: DiskCounters?
private var previousDiskDate: Date?
private var previousProcessCPU: [Int32: TimeInterval] = [:]
private var previousProcessNetwork: [Int32: ProcessNetworkTotals] = [:]
private var previousProcessNetworkDate: Date?
private var previousProcessIO: [Int32: ProcessIOTotals] = [:]
private var previousProcessIODate: Date?
private var appNetworkTask: Task<Void, Never>?
private var lastAppNetworkSample: Date?
init() {
updateSpeed = UpdateSpeed(rawValue: UserDefaults.standard.string(forKey: "updateSpeed") ?? "") ?? .normal
loadAppHistory()
}
var isPaused: Bool {
get { updateSpeed == .paused }
set { updateSpeed = newValue ? .paused : .normal }
}
func start() {
guard updateTask == nil else { return }
refresh()
updateTask = Task { [weak self] in
while !Task.isCancelled {
let interval = self?.updateSpeed.interval ?? .seconds(2)
try? await Task.sleep(for: interval)
guard let self, !self.isPaused else { continue }
self.refresh()
}
}
}
func refresh() {
Task {
let result = await Task.detached(priority: .userInitiated) {
ProcessScanner.capture()
}.value
processes = result.processes
updateCPUHistory(with: result.processes)
updateProcessDiskActivity(result.processIO, current: result.processes)
services = result.services
var refreshedSnapshot = result.snapshot
refreshedSnapshot.corePercents = CoreCPUReader.capture(previous: &previousCoreTicks)
let diskDate = Date()
if let previousDiskCounters, let previousDiskDate {
let elapsed = max(0.001, diskDate.timeIntervalSince(previousDiskDate))
let readBytes = result.disk.readBytes >= previousDiskCounters.readBytes
? result.disk.readBytes - previousDiskCounters.readBytes : 0
let writeBytes = result.disk.writeBytes >= previousDiskCounters.writeBytes
? result.disk.writeBytes - previousDiskCounters.writeBytes : 0
let readOperations = result.disk.readOperations >= previousDiskCounters.readOperations
? result.disk.readOperations - previousDiskCounters.readOperations : 0
let writeOperations = result.disk.writeOperations >= previousDiskCounters.writeOperations
? result.disk.writeOperations - previousDiskCounters.writeOperations : 0
let readTime = result.disk.readTime >= previousDiskCounters.readTime
? result.disk.readTime - previousDiskCounters.readTime : 0
let writeTime = result.disk.writeTime >= previousDiskCounters.writeTime
? result.disk.writeTime - previousDiskCounters.writeTime : 0
let operations = readOperations + writeOperations
let serviceTime = readTime + writeTime
refreshedSnapshot.diskReadRate = Double(readBytes) / elapsed
refreshedSnapshot.diskWriteRate = Double(writeBytes) / elapsed
refreshedSnapshot.diskOperationsPerSecond = Double(operations) / elapsed
refreshedSnapshot.diskLatencyMilliseconds = operations > 0
? Double(serviceTime) / Double(operations) / 1_000_000 : 0
refreshedSnapshot.diskActivePercent = min(100, Double(serviceTime) / (elapsed * 1_000_000_000) * 100)
}
previousDiskCounters = result.disk
previousDiskDate = diskDate
let networkDate = Date()
if let previousNetworkCounters, let previousNetworkDate {
let elapsed = max(0.001, networkDate.timeIntervalSince(previousNetworkDate))
if result.network.received >= previousNetworkCounters.received {
refreshedSnapshot.networkReceiveRate = Double(result.network.received - previousNetworkCounters.received) / elapsed
}
if result.network.sent >= previousNetworkCounters.sent {
refreshedSnapshot.networkSendRate = Double(result.network.sent - previousNetworkCounters.sent) / elapsed
}
}
refreshedSnapshot.networkInterface = result.network.interface
previousNetworkCounters = result.network
previousNetworkDate = networkDate
snapshot = refreshedSnapshot
lastUpdated = Date()
errorMessage = result.error
appendHistory(cpu: snapshot.cpuPercent, memory: snapshot.memoryPercent)
sampleAppNetworkIfNeeded()
}
}
func resetAppHistory() {
appHistory = []
appHistoryStartDate = Date()
previousProcessCPU = Dictionary(uniqueKeysWithValues: processes.map { ($0.pid, $0.cpuTime) })
previousProcessNetwork = [:]
saveAppHistory()
}
func activeProcess(for usage: AppUsageRecord) -> ProcessRecord? {
processes.first { AppIdentity.forProcess($0).id == usage.id }
}
func terminate(_ process: ProcessRecord, force: Bool = false) {
guard process.pid != getpid() else {
errorMessage = "Task Manager cannot end itself."
return
}
let signal = force ? SIGKILL : SIGTERM
if kill(process.pid, signal) == 0 {
processes.removeAll { $0.pid == process.pid }
Task {
try? await Task.sleep(for: .milliseconds(400))
refresh()
}
} else {
errorMessage = "Could not end \(process.displayName). You may not have permission."
}
}
func terminateTree(_ process: ProcessRecord) {
let descendants = descendantPIDs(of: process.pid)
for pid in descendants.reversed() { _ = kill(pid, SIGTERM) }
terminate(process)
}
func terminateGroup(_ group: [ProcessRecord]) {
guard !group.contains(where: { $0.pid == getpid() }) else {
errorMessage = "Task Manager cannot end an application group that contains itself."
return
}
let pids = Set(group.map(\.pid))
var failed = false
for process in group {
if kill(process.pid, SIGTERM) != 0 { failed = true }
}
processes.removeAll { pids.contains($0.pid) }
if failed { errorMessage = "Some processes could not be ended. You may not have permission." }
Task {
try? await Task.sleep(for: .milliseconds(500))
refresh()
}
}
func sendSignal(_ signal: Int32, to process: ProcessRecord, successMessage: String? = nil) {
guard process.pid != getpid() else {
errorMessage = "Task Manager cannot control itself."
return
}
if kill(process.pid, signal) != 0 {
errorMessage = "Could not control \(process.displayName). You may not have permission."
} else if let successMessage {
errorMessage = successMessage
}
Task {
try? await Task.sleep(for: .milliseconds(300))
refresh()
}
}
func setPriority(_ priority: Int, for process: ProcessRecord) {
let command = Process()
command.executableURL = URL(fileURLWithPath: "/usr/bin/renice")
command.arguments = ["-n", "\(priority)", "-p", "\(process.pid)"]
command.standardOutput = FileHandle.nullDevice
command.standardError = FileHandle.nullDevice
do {
try command.run()
command.waitUntilExit()
if command.terminationStatus != 0 {
errorMessage = "Could not change priority for \(process.displayName). Higher priorities may require administrator access."
}
} catch {
errorMessage = error.localizedDescription
}
}
func startService(_ service: ServiceRecord) {
runServiceCommand(["kickstart", serviceTarget(service)], action: "start", service: service)
}
func stopService(_ service: ServiceRecord) {
runServiceCommand(["kill", "SIGTERM", serviceTarget(service)], action: "stop", service: service)
}
func restartService(_ service: ServiceRecord) {
runServiceCommand(["kickstart", "-k", serviceTarget(service)], action: "restart", service: service)
}
private func serviceTarget(_ service: ServiceRecord) -> String {
"\(service.domain.launchctlPrefix)/\(service.label)"
}
private func runServiceCommand(_ arguments: [String], action: String, service: ServiceRecord) {
let command = Process()
let errorPipe = Pipe()
command.executableURL = URL(fileURLWithPath: "/bin/launchctl")
command.arguments = arguments
command.standardOutput = FileHandle.nullDevice
command.standardError = errorPipe
do {
try command.run()
command.waitUntilExit()
if command.terminationStatus != 0 {
let message = String(decoding: errorPipe.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
.trimmingCharacters(in: .whitespacesAndNewlines)
errorMessage = message.isEmpty ? "Could not \(action) \(service.displayName)." : message
}
Task {
try? await Task.sleep(for: .milliseconds(500))
refresh()
}
} catch {
errorMessage = error.localizedDescription
}
}
private func descendantPIDs(of parentPID: Int32) -> [Int32] {
let children = processes.filter { $0.parentPID == parentPID }.map(\.pid)
return children + children.flatMap { descendantPIDs(of: $0) }
}
private func appendHistory(cpu: Double, memory: Double) {
let now = Date()
cpuHistory.append(MetricSample(date: now, value: cpu))
memoryHistory.append(MetricSample(date: now, value: memory))
networkReceiveHistory.append(MetricSample(date: now, value: snapshot.networkReceiveRate))
networkSendHistory.append(MetricSample(date: now, value: snapshot.networkSendRate))
diskReadHistory.append(MetricSample(date: now, value: snapshot.diskReadRate))
diskWriteHistory.append(MetricSample(date: now, value: snapshot.diskWriteRate))
diskLatencyHistory.append(MetricSample(date: now, value: snapshot.diskLatencyMilliseconds))
diskActiveHistory.append(MetricSample(date: now, value: snapshot.diskActivePercent))
gpuHistory.append(MetricSample(date: now, value: snapshot.gpuPercent))
gpuRendererHistory.append(MetricSample(date: now, value: snapshot.gpuRendererPercent))
gpuTilerHistory.append(MetricSample(date: now, value: snapshot.gpuTilerPercent))
if coreHistories.count != snapshot.corePercents.count {
coreHistories = Array(repeating: [], count: snapshot.corePercents.count)
}
for index in snapshot.corePercents.indices {
coreHistories[index].append(MetricSample(date: now, value: snapshot.corePercents[index]))
}
let cutoff = now.addingTimeInterval(-60)
cpuHistory.removeAll { $0.date < cutoff }
memoryHistory.removeAll { $0.date < cutoff }
networkReceiveHistory.removeAll { $0.date < cutoff }
networkSendHistory.removeAll { $0.date < cutoff }
diskReadHistory.removeAll { $0.date < cutoff }
diskWriteHistory.removeAll { $0.date < cutoff }
diskLatencyHistory.removeAll { $0.date < cutoff }
diskActiveHistory.removeAll { $0.date < cutoff }
gpuHistory.removeAll { $0.date < cutoff }
gpuRendererHistory.removeAll { $0.date < cutoff }
gpuTilerHistory.removeAll { $0.date < cutoff }
for index in coreHistories.indices {
coreHistories[index].removeAll { $0.date < cutoff }
}
}
private func updateCPUHistory(with current: [ProcessRecord]) {
var records = Dictionary(uniqueKeysWithValues: appHistory.map { ($0.id, $0) })
let now = Date()
for process in current where process.user == NSUserName() {
let identity = AppIdentity.forProcess(process)
var usage = records[identity.id] ?? AppUsageRecord(
id: identity.id,
name: identity.name,
executablePath: identity.path,
user: process.user,
cpuSeconds: 0,
networkReceivedBytes: 0,
networkSentBytes: 0,
lastSeen: now
)
if let old = previousProcessCPU[process.pid], process.cpuTime >= old {
usage.cpuSeconds += process.cpuTime - old
}
usage.lastSeen = now
records[identity.id] = usage
}
previousProcessCPU = Dictionary(uniqueKeysWithValues: current.map { ($0.pid, $0.cpuTime) })
appHistory = Array(records.values)
saveAppHistory()
}
private func sampleAppNetworkIfNeeded() {
guard appNetworkTask == nil,
lastAppNetworkSample.map({ Date().timeIntervalSince($0) >= 10 }) ?? true else { return }
lastAppNetworkSample = Date()
appNetworkTask = Task { [weak self] in
let totals = await Task.detached(priority: .utility) { AppNetworkScanner.capture() }.value
guard let self else { return }
self.mergeNetworkHistory(totals)
self.appNetworkTask = nil
}
}
private func mergeNetworkHistory(_ totals: [Int32: ProcessNetworkTotals]) {
var records = Dictionary(uniqueKeysWithValues: appHistory.map { ($0.id, $0) })
let processByPID = Dictionary(uniqueKeysWithValues: processes.map { ($0.pid, $0) })
let now = Date()
let elapsed = previousProcessNetworkDate.map { max(0.001, now.timeIntervalSince($0)) }
for (pid, total) in totals {
guard let process = processByPID[pid], process.user == NSUserName() else { continue }
let identity = AppIdentity.forProcess(process)
guard var usage = records[identity.id] else { continue }
if let old = previousProcessNetwork[pid] {
if total.received >= old.received { usage.networkReceivedBytes += total.received - old.received }
if total.sent >= old.sent { usage.networkSentBytes += total.sent - old.sent }
}
records[identity.id] = usage
}
if let elapsed {
for (pid, total) in totals {
guard let old = previousProcessNetwork[pid] else { continue }
var activity = processActivity[pid] ?? ProcessActivityRate()
activity.networkReceive = total.received >= old.received ? Double(total.received - old.received) / elapsed : 0
activity.networkSend = total.sent >= old.sent ? Double(total.sent - old.sent) / elapsed : 0
processActivity[pid] = activity
}
}
previousProcessNetwork = totals
previousProcessNetworkDate = now
appHistory = Array(records.values)
saveAppHistory()
}
private func updateProcessDiskActivity(_ totals: [Int32: ProcessIOTotals], current: [ProcessRecord]) {
let now = Date()
let activePIDs = Set(current.map(\.pid))
processActivity = processActivity.filter { activePIDs.contains($0.key) }
if let previousProcessIODate {
let elapsed = max(0.001, now.timeIntervalSince(previousProcessIODate))
for (pid, total) in totals {
guard let old = previousProcessIO[pid] else { continue }
var activity = processActivity[pid] ?? ProcessActivityRate()
activity.diskRead = total.read >= old.read ? Double(total.read - old.read) / elapsed : 0
activity.diskWrite = total.written >= old.written ? Double(total.written - old.written) / elapsed : 0
processActivity[pid] = activity
}
}
previousProcessIO = totals
previousProcessIODate = now
}
private func loadAppHistory() {
guard let data = try? Data(contentsOf: AppHistoryPersistence.url),
let state = try? JSONDecoder().decode(AppHistoryPersistence.State.self, from: data) else { return }
appHistoryStartDate = state.startDate
appHistory = state.records
}
private func saveAppHistory() {
let state = AppHistoryPersistence.State(startDate: appHistoryStartDate, records: appHistory)
guard let data = try? JSONEncoder().encode(state) else { return }
do {
try FileManager.default.createDirectory(at: AppHistoryPersistence.directory, withIntermediateDirectories: true)
try data.write(to: AppHistoryPersistence.url, options: .atomic)
} catch {
errorMessage = "App history could not be saved: \(error.localizedDescription)"
}
}
}
private enum AppHistoryPersistence {
struct State: Codable {
let startDate: Date
let records: [AppUsageRecord]
}
static let directory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("MacTaskManager", isDirectory: true)
static let url = directory.appendingPathComponent("app-history.json")
}
private struct AppIdentity {
let id: String
let name: String
let path: String
static func forProcess(_ process: ProcessRecord) -> AppIdentity {
let path = process.executablePath
if let range = path.range(of: ".app/Contents/", options: .caseInsensitive) {
let appPath = String(path[..<range.lowerBound]) + ".app"
let filename = URL(fileURLWithPath: appPath).deletingPathExtension().lastPathComponent
return AppIdentity(id: appPath, name: filename, path: appPath)
}
return AppIdentity(id: "\(process.user)|\(path)", name: process.displayName, path: path)
}
}
private struct ProcessNetworkTotals: Sendable {
let received: UInt64
let sent: UInt64
}
private struct ProcessIOTotals: Sendable {
let read: UInt64
let written: UInt64
}
private enum AppNetworkScanner {
static func capture() -> [Int32: ProcessNetworkTotals] {
let process = Process()
let pipe = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/nettop")
process.arguments = ["-P", "-x", "-l", "1", "-J", "bytes_in,bytes_out"]
process.standardOutput = pipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
guard process.terminationStatus == 0 else { return [:] }
return parse(String(decoding: data, as: UTF8.self))
} catch {
return [:]
}
}
private static func parse(_ output: String) -> [Int32: ProcessNetworkTotals] {
var result: [Int32: ProcessNetworkTotals] = [:]
for line in output.split(separator: "\n").dropFirst() {
let fields = line.split(whereSeparator: { $0 == " " || $0 == "\t" })
guard fields.count >= 3,
let received = UInt64(fields[fields.count - 2]),
let sent = UInt64(fields[fields.count - 1]) else { continue }
let identity = fields.dropLast(2).joined(separator: " ")
guard let dot = identity.lastIndex(of: "."), let pid = Int32(identity[identity.index(after: dot)...]) else { continue }
result[pid] = ProcessNetworkTotals(received: received, sent: sent)
}
return result
}
}
private struct NetworkCounters: Sendable {
let received: UInt64
let sent: UInt64
let interface: String
}
private struct DiskCounters: Sendable {
var readBytes: UInt64 = 0
var writeBytes: UInt64 = 0
var readOperations: UInt64 = 0
var writeOperations: UInt64 = 0
var readTime: UInt64 = 0
var writeTime: UInt64 = 0
}
private struct GPUCounters: Sendable {
var devicePercent = 0.0
var rendererPercent = 0.0
var tilerPercent = 0.0
var memoryBytes: UInt64 = 0
var allocatedBytes: UInt64 = 0
var coreCount = 0
}
private struct MemoryCounters: Sendable {
var used: UInt64 = 0
var active: UInt64 = 0
var wired: UInt64 = 0
var compressed: UInt64 = 0
var cached: UInt64 = 0
var swapUsed: UInt64 = 0
var swapTotal: UInt64 = 0
}
private struct MemoryHardwareInfo: Sendable {
var type = "Unified"
var speed = "Not reported by macOS"
var manufacturer = "Apple unified memory"
}
private struct CoreTicks {
let active: UInt64
let idle: UInt64
}
private enum CoreCPUReader {
static func capture(previous: inout [CoreTicks]) -> [Double] {
var processorCount: natural_t = 0
var info: processor_info_array_t?
var infoCount: mach_msg_type_number_t = 0
let result = host_processor_info(
mach_host_self(),
PROCESSOR_CPU_LOAD_INFO,
&processorCount,
&info,
&infoCount
)
guard result == KERN_SUCCESS, let info else { return [] }
defer {
vm_deallocate(
mach_task_self_,
vm_address_t(UInt(bitPattern: info)),
vm_size_t(infoCount) * vm_size_t(MemoryLayout<integer_t>.stride)
)
}
let current = (0..<Int(processorCount)).map { core -> CoreTicks in
let offset = core * Int(CPU_STATE_MAX)
let user = UInt64(info[offset + Int(CPU_STATE_USER)])
let system = UInt64(info[offset + Int(CPU_STATE_SYSTEM)])
let nice = UInt64(info[offset + Int(CPU_STATE_NICE)])
let idle = UInt64(info[offset + Int(CPU_STATE_IDLE)])
return CoreTicks(active: user + system + nice, idle: idle)
}
defer { previous = current }
guard previous.count == current.count else { return Array(repeating: 0, count: current.count) }
return zip(current, previous).map { current, old in
let activeDelta = current.active >= old.active ? current.active - old.active : 0
let idleDelta = current.idle >= old.idle ? current.idle - old.idle : 0
let total = activeDelta + idleDelta
return total > 0 ? min(100, Double(activeDelta) / Double(total) * 100) : 0
}
}
}
private enum ProcessScanner {
private static let memoryHardware = memoryHardwareInfo()
struct Result: Sendable {
let processes: [ProcessRecord]
let services: [ServiceRecord]
let snapshot: SystemSnapshot
let network: NetworkCounters
let disk: DiskCounters
let processIO: [Int32: ProcessIOTotals]
let error: String?
}
static func capture() -> Result {
let ps = run("/bin/ps", arguments: ["-axo", "pid=,ppid=,user=,%cpu=,%mem=,rss=,state=,etime=,time=,pri=,nice=,comm="])
let threads = threadCounts()
let records = parseProcesses(ps.output, threadCounts: threads)
let userServices = parseServices(run("/bin/launchctl", arguments: ["list"]).output, domain: .user)
let systemServices = parseSystemServices(run("/bin/launchctl", arguments: ["print", "system"]).output)
let services = userServices + systemServices
let cpu = min(100, records.reduce(0) { $0 + $1.cpu })
let totalMemory = ProcessInfo.processInfo.physicalMemory
let memory = memoryCounters(total: totalMemory)
let disk = diskUsage()
var snapshot = SystemSnapshot()
snapshot.cpuPercent = cpu
snapshot.memoryUsed = memory.used
snapshot.memoryTotal = totalMemory
snapshot.memoryActive = memory.active
snapshot.memoryWired = memory.wired
snapshot.memoryCompressed = memory.compressed
snapshot.memoryCached = memory.cached
snapshot.swapUsed = memory.swapUsed
snapshot.swapTotal = memory.swapTotal
snapshot.memoryType = memoryHardware.type
snapshot.memorySpeed = memoryHardware.speed
snapshot.memoryManufacturer = memoryHardware.manufacturer
snapshot.diskFree = disk.free
snapshot.diskTotal = disk.total
snapshot.removableVolumes = removableVolumes()
snapshot.processCount = records.count
snapshot.threadCount = threads.values.reduce(0, +)
snapshot.uptime = ProcessInfo.processInfo.systemUptime
let gpu = gpuCounters()
snapshot.gpuPercent = gpu.devicePercent
snapshot.gpuRendererPercent = gpu.rendererPercent
snapshot.gpuTilerPercent = gpu.tilerPercent
snapshot.gpuMemoryBytes = gpu.memoryBytes
snapshot.gpuAllocatedBytes = gpu.allocatedBytes
snapshot.gpuCoreCount = gpu.coreCount
return Result(
processes: records,
services: services,
snapshot: snapshot,
network: networkCounters(),
disk: diskCounters(),
processIO: processIOTotals(records.map(\.pid)),
error: ps.error
)
}
private static func processIOTotals(_ pids: [Int32]) -> [Int32: ProcessIOTotals] {
var totals: [Int32: ProcessIOTotals] = [:]
for pid in pids {
var usage = rusage_info_v2()
let result = withUnsafeMutablePointer(to: &usage) { pointer in
pointer.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) {
proc_pid_rusage(pid, RUSAGE_INFO_V2, $0)
}
}
if result == 0 {
totals[pid] = ProcessIOTotals(read: usage.ri_diskio_bytesread, written: usage.ri_diskio_byteswritten)
}
}
return totals
}
private static func diskCounters() -> DiskCounters {
let output = run(
"/usr/sbin/ioreg",
arguments: ["-r", "-c", "IOBlockStorageDriver", "-k", "Statistics", "-a"]
).output
guard let data = output.data(using: .utf8),
let drivers = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [[String: Any]] else {
return DiskCounters()
}
var result = DiskCounters()
for driver in drivers {
guard let statistics = driver["Statistics"] as? [String: Any] else { continue }
func value(_ key: String) -> UInt64 {
(statistics[key] as? NSNumber)?.uint64Value ?? 0
}
result.readBytes += value("Bytes (Read)")
result.writeBytes += value("Bytes (Write)")
result.readOperations += value("Operations (Read)")
result.writeOperations += value("Operations (Write)")
result.readTime += value("Total Time (Read)")
result.writeTime += value("Total Time (Write)")
}
return result
}
private static func gpuCounters() -> GPUCounters {
let output = run(
"/usr/sbin/ioreg",
arguments: ["-r", "-c", "AGXAccelerator", "-l"]
).output
func number(_ key: String) -> UInt64 {
UInt64(output.firstMatch("\"\(NSRegularExpression.escapedPattern(for: key))\" *= *(\\d+)") ?? "0") ?? 0
}
return GPUCounters(
devicePercent: Double(number("Device Utilization %")),
rendererPercent: Double(number("Renderer Utilization %")),
tilerPercent: Double(number("Tiler Utilization %")),
memoryBytes: number("In use system memory"),
allocatedBytes: number("Alloc system memory"),
coreCount: Int(number("gpu-core-count"))
)
}
private static func parseProcesses(_ output: String, threadCounts: [Int32: Int]) -> [ProcessRecord] {
output.split(separator: "\n").compactMap { line in
let fields = line.split(maxSplits: 11, whereSeparator: { $0 == " " || $0 == "\t" })
guard fields.count == 12,
let pid = Int32(fields[0]),
let ppid = Int32(fields[1]),
let cpu = Double(fields[3]),
let memory = Double(fields[4]),
let rssKB = UInt64(fields[5]),
let priority = Int(fields[9]),
let nice = Int(fields[10]) else { return nil }
let path = String(fields[11])
let name = URL(fileURLWithPath: path).lastPathComponent
let normalizedCPU = cpu / Double(max(1, ProcessInfo.processInfo.activeProcessorCount))
return ProcessRecord(
pid: pid,
parentPID: ppid,
name: name,
executablePath: path,
user: String(fields[2]),
cpu: normalizedCPU,
memoryPercent: memory,
residentBytes: rssKB * 1024,
state: String(fields[6]),
elapsed: String(fields[7]),
cpuTime: parseCPUTime(String(fields[8])),
threadCount: threadCounts[pid] ?? 0,
openFileCount: openFileCount(pid),
architecture: BinaryArchitectureReader.architecture(at: path),
priority: priority,
nice: nice
)
}
}
private static func parseCPUTime(_ value: String) -> TimeInterval {
let dayParts = value.split(separator: "-", maxSplits: 1).map(String.init)
let days = dayParts.count == 2 ? Double(dayParts[0]) ?? 0 : 0
let clock = (dayParts.count == 2 ? dayParts[1] : dayParts[0]).split(separator: ":").compactMap { Double($0) }
guard !clock.isEmpty else { return days * 86_400 }
var multiplier = 1.0
var seconds = 0.0
for component in clock.reversed() {
seconds += component * multiplier
multiplier *= 60
}
return days * 86_400 + seconds
}
private static func parseServices(_ output: String, domain: ServiceDomain) -> [ServiceRecord] {
output.split(separator: "\n").dropFirst().compactMap { line in
let fields = line.split(maxSplits: 2, whereSeparator: { $0 == " " || $0 == "\t" })
guard fields.count == 3, let status = Int32(fields[1]) else { return nil }
let pid = fields[0] == "-" ? nil : Int32(fields[0])
let label = String(fields[2]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !label.isEmpty else { return nil }
return ServiceRecord(label: label, pid: pid, lastExitStatus: status, domain: domain)
}
}
private static func parseSystemServices(_ output: String) -> [ServiceRecord] {
guard let start = output.range(of: "\n\tservices = {\n") else { return [] }
let body = output[start.upperBound...]
guard let end = body.range(of: "\n\t}") else { return [] }
return body[..<end.lowerBound].split(separator: "\n").compactMap { line in
let fields = line.split(maxSplits: 2, whereSeparator: { $0 == " " || $0 == "\t" })
guard fields.count == 3, let rawPID = Int32(fields[0]) else { return nil }
let status = Int32(fields[1]) ?? 0
let label = String(fields[2]).trimmingCharacters(in: .whitespacesAndNewlines)
guard !label.isEmpty else { return nil }
return ServiceRecord(
label: label,
pid: rawPID > 0 ? rawPID : nil,
lastExitStatus: status,
domain: .system
)
}
}
private static func memoryCounters(total: UInt64) -> MemoryCounters {
let vm = run("/usr/bin/vm_stat", arguments: []).output
let pageSize = UInt64(vm.firstMatch(#"page size of (\d+) bytes"#) ?? "4096") ?? 4096
func pages(_ label: String) -> UInt64 {
UInt64(vm.firstMatch("\(NSRegularExpression.escapedPattern(for: label)): +([0-9]+)") ?? "0") ?? 0
}
let free = (pages("Pages free") + pages("Pages speculative")) * pageSize
var swap = xsw_usage()
var swapSize = MemoryLayout<xsw_usage>.size
_ = sysctlbyname("vm.swapusage", &swap, &swapSize, nil, 0)
return MemoryCounters(
used: total > free ? total - free : 0,
active: pages("Pages active") * pageSize,
wired: pages("Pages wired down") * pageSize,
compressed: pages("Pages occupied by compressor") * pageSize,
cached: (pages("Pages inactive") + pages("Pages purgeable")) * pageSize,
swapUsed: swap.xsu_used,
swapTotal: swap.xsu_total
)
}
private static func memoryHardwareInfo() -> MemoryHardwareInfo {
let output = run("/usr/sbin/system_profiler", arguments: ["SPMemoryDataType", "-detailLevel", "mini"]).output
return MemoryHardwareInfo(
type: output.firstMatch(#"Type: +([^\n]+)"#) ?? "Unified",
speed: output.firstMatch(#"Speed: +([^\n]+)"#) ?? "Not reported by macOS",
manufacturer: output.firstMatch(#"Manufacturer: +([^\n]+)"#) ?? "Apple unified memory"
)
}
private static func removableVolumes() -> [VolumeSnapshot] {
let keys: [URLResourceKey] = [
.volumeNameKey,
.volumeLocalizedFormatDescriptionKey,
.volumeIsRemovableKey,
.volumeIsEjectableKey,
.volumeTotalCapacityKey,
.volumeAvailableCapacityForImportantUsageKey
]
let urls = FileManager.default.mountedVolumeURLs(
includingResourceValuesForKeys: keys,
options: [.skipHiddenVolumes]
) ?? []
return urls.compactMap { url in
guard let values = try? url.resourceValues(forKeys: Set(keys)),
values.volumeIsRemovable == true || values.volumeIsEjectable == true else { return nil }
let capacity = UInt64(max(0, values.volumeTotalCapacity ?? 0))
let available = UInt64(max(0, values.volumeAvailableCapacityForImportantUsage ?? 0))
return VolumeSnapshot(
id: url.path,
name: values.volumeName ?? url.lastPathComponent,
mountPath: url.path,
fileSystem: values.volumeLocalizedFormatDescription ?? "Removable storage",
capacity: capacity,
available: available,
isEjectable: values.volumeIsEjectable ?? false
)
}
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
private static func diskUsage() -> (free: UInt64, total: UInt64) {
guard let values = try? URL(fileURLWithPath: "/").resourceValues(forKeys: [
.volumeAvailableCapacityForImportantUsageKey,
.volumeTotalCapacityKey
]) else { return (0, 0) }
return (UInt64(max(0, values.volumeAvailableCapacityForImportantUsage ?? 0)), UInt64(max(0, values.volumeTotalCapacity ?? 0)))
}
private static func threadCounts() -> [Int32: Int] {
let result = run("/bin/ps", arguments: ["-M", "-axo", "pid="]).output
let rawCounts = result.split(separator: "\n").reduce(into: [Int32: Int]()) { counts, line in
guard let last = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).last,
let pid = Int32(last) else { return }
counts[pid, default: 0] += 1
}
return rawCounts.mapValues { max(1, $0 - 1) }
}
private static func openFileCount(_ pid: Int32) -> Int {
let bytes = Int(proc_pidinfo(pid, PROC_PIDLISTFDS, 0, nil, 0))
return bytes > 0 ? bytes / MemoryLayout<proc_fdinfo>.stride : 0
}
private static func networkCounters() -> NetworkCounters {
let route = run("/sbin/route", arguments: ["-n", "get", "default"]).output
let primary = route.firstMatch(#"interface: +([^\s]+)"#) ?? "Network"
let netstat = run("/usr/sbin/netstat", arguments: ["-ibn"]).output
var received: UInt64 = 0
var sent: UInt64 = 0
var matchedPrimary = false
for line in netstat.split(separator: "\n") {
let fields = line.split(whereSeparator: { $0 == " " || $0 == "\t" }).map(String.init)
guard fields.count >= 10,
fields[2].hasPrefix("<Link#"),
fields[0] != "lo0",
!fields[0].hasSuffix("*") else { continue }
if primary != "Network", fields[0] != primary { continue }
let counters = Array(fields.suffix(7))
guard let inputBytes = UInt64(counters[2]), let outputBytes = UInt64(counters[5]) else { continue }
received += inputBytes
sent += outputBytes
matchedPrimary = true
}
if !matchedPrimary, primary != "Network" {
return NetworkCounters(received: 0, sent: 0, interface: primary)
}
return NetworkCounters(received: received, sent: sent, interface: primary)
}
private static func run(_ path: String, arguments: [String]) -> (output: String, error: String?) {
let process = Process()
let pipe = Pipe()
process.executableURL = URL(fileURLWithPath: path)
process.arguments = arguments
process.standardOutput = pipe
process.standardError = FileHandle.nullDevice
do {
try process.run()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
process.waitUntilExit()
return (String(decoding: data, as: UTF8.self), process.terminationStatus == 0 ? nil : "System data is temporarily unavailable.")
} catch {
return ("", error.localizedDescription)
}
}
}
private enum BinaryArchitectureReader {
private static let machO64: UInt32 = 0xfeedfacf
private static let machO64Swapped: UInt32 = 0xcffaedfe
private static let fat32: UInt32 = 0xcafebabe
private static let fat32Swapped: UInt32 = 0xbebafeca
private static let fat64: UInt32 = 0xcafebabf
private static let fat64Swapped: UInt32 = 0xbfbafeca
private static let arm64: UInt32 = 0x0100000c
private static let x86_64: UInt32 = 0x01000007
static func architecture(at path: String) -> String {
guard path.hasPrefix("/"),
let handle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: path)) else { return "Unknown" }
defer { try? handle.close() }
guard let data = try? handle.read(upToCount: 512), data.count >= 8 else { return "Unknown" }
let magicBE = read(data, offset: 0, bigEndian: true)
let magicLE = read(data, offset: 0, bigEndian: false)
if magicBE == fat32 || magicBE == fat64 || magicBE == fat32Swapped || magicBE == fat64Swapped {
let bigEndian = magicBE == fat32 || magicBE == fat64
let is64 = magicBE == fat64 || magicBE == fat64Swapped
let count = min(Int(read(data, offset: 4, bigEndian: bigEndian)), 16)
let stride = is64 ? 32 : 20
var architectures: Set<UInt32> = []
for index in 0..<count {
let offset = 8 + index * stride
guard offset + 4 <= data.count else { break }
architectures.insert(read(data, offset: offset, bigEndian: bigEndian))
}
if architectures.contains(arm64) { return "ARM64" }
if architectures.contains(x86_64) { return "x86_64" }
return "Universal"
}
if magicLE == machO64 || magicBE == machO64Swapped {
let type = read(data, offset: 4, bigEndian: false)
if type == arm64 { return "ARM64" }
if type == x86_64 { return "x86_64" }
}
return "Unknown"
}
private static func read(_ data: Data, offset: Int, bigEndian: Bool) -> UInt32 {
guard offset + 4 <= data.count else { return 0 }
let bytes = data[offset..<(offset + 4)]
if bigEndian {
return bytes.reduce(0) { ($0 << 8) | UInt32($1) }
}
return bytes.reversed().reduce(0) { ($0 << 8) | UInt32($1) }
}
}
private extension String {
func firstMatch(_ pattern: String) -> String? {
guard let regex = try? NSRegularExpression(pattern: pattern),
let match = regex.firstMatch(in: self, range: NSRange(startIndex..., in: self)),
match.numberOfRanges > 1,
let range = Range(match.range(at: 1), in: self) else { return nil }
return String(self[range])
}
}