Sechs neue Widgets (CPU, GPU, Speicher, Akku, Sensoren, Netzwerk) und dieselben sechs als einzeln aktivierbare Menüleisten-Module mit fünf Darstellungsarten. Panel-Widget und Menüleisten-Modul derselben Größe teilen sich die Messschleife über Referenzzählung. Ohne das liefe für jede Anzeige ein eigener Timer mit denselben IOKit- und SMC-Abfragen — bei sechs Modulen und ebenso vielen Widgets ein Vielfaches der nötigen Arbeit. Meldet sich der letzte Konsument ab, hört die Schleife auf; im Ruhezustand misst Onyx nichts. Die Differenzrechnung ist testgetrieben, und die Tests haben zwei echte Fehler gefunden. Erstens: mein Überlaufschutz beim Durchsatz hielt einen zurückgesetzten Zähler (Interface-Wechsel) für einen Überlauf und zeigte 4,3 GB/s. Da if_data64 64-Bit-Zähler liefert, die bei 10 Gbit/s erst nach 470 Jahren umlaufen, ist ein kleinerer Wert immer ein Zurücksetzen — die Unterscheidung produzierte nur den Ausreißer, den sie verhindern sollte. Zweitens: ByteCountFormatter schrieb bei null „Zero KB/s" statt „0 KB/s". Bei den CPU-Zählern bleibt der Überlaufschutz nötig: die sind 32 Bit breit und laufen nach gut 400 Tagen wirklich um, deshalb `&-` statt `-`. Sensoren kommen über den SMC-Leser aus Spike A statt über die private IOHID-Schnittstelle — auf dieser Maschine nachweislich verifiziert, und die dort gefundene Little-Endian-Eigenheit ist berücksichtigt. Nur eine kurze Liste benannter Fühler statt aller 3486 Keys: eine Wand aus Kürzeln ist keine Information. Gegengemessen statt vermutet: 15 Kerne, 39,8 W, GPU 50 %, 16,6/24 GB mit kritischem Druck, Akku 68 Zyklen bei 100 % Gesundheit, fünf Sensoren, zwei Lüfter, en0 mit 661 KB/s. Menüleisten-Module zeichnen in labelColor statt in den Onyx-Farben — nur die Systemfarbe passt sich heller wie dunkler Leiste an. Feste Breite je Darstellungsart, sonst schiebt jeder Messwert die halbe Leiste hin und her. Standardmäßig ist kein Modul aktiv: sechs neue Symbole beim ersten Start wären eine Zumutung. 145 Tests grün.
116 lines
4.6 KiB
Swift
116 lines
4.6 KiB
Swift
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())) %"
|
|
}
|
|
|
|
public static func temperature(_ celsius: Double) -> String {
|
|
"\(Int(celsius.rounded()))°"
|
|
}
|
|
|
|
public static func watts(_ value: Double) -> String {
|
|
String(format: "%.1f W", max(value, 0))
|
|
}
|
|
}
|