import Foundation /// Die CPU-Zeitzähler eines Kerns, wie `host_processor_info` sie liefert. /// /// Es sind **Summen seit dem Start**, keine Momentanwerte. Die Auslastung /// ergibt sich erst aus der Differenz zweier Abtastungen. public struct CPUTicks: Equatable, Sendable { public let user: UInt32 public let system: UInt32 public let idle: UInt32 public let nice: UInt32 public init(user: UInt32, system: UInt32, idle: UInt32, nice: UInt32) { self.user = user self.system = system self.idle = idle self.nice = nice } /// Auslastung zwischen zwei Abtastungen, 0 bis 1. public static func usage(from previous: CPUTicks, to current: CPUTicks) -> Double { // `&-` statt `-`: die Zähler sind 32 Bit breit und laufen nach gut // 400 Tagen über. Ein gewöhnlicher Abzug stürzt dabei ab oder liefert // einen absurden Sprung; die überlaufende Subtraktion ergibt genau die // richtige Differenz. let user = UInt64(current.user &- previous.user) let system = UInt64(current.system &- previous.system) let nice = UInt64(current.nice &- previous.nice) let idle = UInt64(current.idle &- previous.idle) let busy = user + system + nice let total = busy + idle guard total > 0 else { return 0 } return min(max(Double(busy) / Double(total), 0), 1) } } /// Wie angespannt die Speicherlage ist. public enum MemoryPressure: Equatable, Sendable { case normal case warning case critical /// - Parameters: /// - free: Anteil wirklich freien Speichers, 0 bis 1. /// - compressed: Anteil komprimierten Speichers, 0 bis 1. /// - swapUsed: belegter Auslagerungsspeicher in Byte. public static func classify(free: Double, compressed: Double, swapUsed: UInt64) -> MemoryPressure { // Auslagerung schlägt alles andere: sobald sie läuft, bremst der // Rechner spürbar — auch wenn die Freispeicheranzeige harmlos aussieht. // Ein bisschen Swap hat macOS fast immer angelegt, deshalb erst ab // einem halben Gigabyte. if swapUsed > 512 * 1024 * 1024 { return .critical } if free < 0.10 || compressed > 0.20 { return .warning } return .normal } } /// Netzwerkdurchsatz aus zwei Zählerständen. public enum Throughput { /// Byte pro Sekunde zwischen zwei Abtastungen. /// /// Die Normierung auf die tatsächlich verstrichene Zeit ist der Kern: die /// Menüleisten-Module tasten je nach Einstellung alle 1 bis 10 Sekunden ab. /// Ohne sie zeigte ein Modul mit 5 Sekunden Intervall das Fünffache an. public static func rate(previous: UInt64, current: UInt64, elapsed: TimeInterval) -> Double { guard elapsed > 0 else { return 0 } // Zähler kleiner als vorher heißt: er wurde zurückgesetzt. Das passiert // bei jedem Interface-Wechsel — WLAN aus, VPN an — und beim Neustart // des Dienstes. // // Ein Überlauf wäre die andere denkbare Erklärung, kommt hier aber // nicht vor: `if_data64` liefert 64-Bit-Zähler, die bei 10 Gbit/s // erst nach rund 470 Jahren umlaufen. Der Versuch, beides zu // unterscheiden, produziert nur den Ausreißer, den er verhindern soll — // deshalb null. guard current >= previous else { return 0 } return Double(current - previous) / elapsed } /// „2,4 MB/s" statt „2400000". public static func formatted(_ bytesPerSecond: Double) -> String { let formatter = ByteCountFormatter() formatter.countStyle = .binary formatter.allowedUnits = [.useKB, .useMB, .useGB] formatter.zeroPadsFractionDigits = false // Ohne das steht bei null „Zero KB/s" statt „0 KB/s" — in einer Spalte // aus Zahlen fällt ein Wort unangenehm auf und ändert die Breite. formatter.allowsNonnumericFormatting = false return formatter.string(fromByteCount: Int64(max(bytesPerSecond, 0))) + "/s" } } /// Einheitliche Zahlendarstellung für alle Messwerte. public enum MetricFormat { /// Ganzzahlig. Nachkommastellen bei der Auslastung täuschen eine /// Genauigkeit vor, die die Messung nicht hat — und sie zappeln im /// Sekundentakt, was die Anzeige unruhig macht. public static func percent(_ fraction: Double) -> String { "\(Int((min(max(fraction, 0), 1) * 100).rounded())) %" } /// Ohne Leerzeichen — nur für die Menüleiste. /// /// Dort ist die Breite fest für „100 %" reserviert, damit nichts springt, /// wenn der Wert eine Stelle mehr bekommt. Das Leerzeichen kostet in dieser /// Reservierung drei Punkte je Element, und in der Menüleiste ist Platz /// das knappste Gut. Im Panel und in den Popovern bleibt es stehen. public static func percentCompact(_ fraction: Double) -> String { "\(Int((min(max(fraction, 0), 1) * 100).rounded()))%" } public static func temperature(_ celsius: Double) -> String { "\(Int(celsius.rounded()))°" } public static func watts(_ value: Double) -> String { String(format: "%.1f W", max(value, 0)) } }