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.
109 lines
3.5 KiB
Swift
109 lines
3.5 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
/// Ein Messwertstrom mit Referenzzählung.
|
|
///
|
|
/// Panel-Widget und Menüleisten-Modul greifen auf **dieselbe** Messschleife zu.
|
|
/// Ohne das liefe für jede Anzeige ein eigener Timer, der dieselben IOKit- und
|
|
/// SMC-Abfragen doppelt macht — bei sechs Modulen und ebenso vielen Widgets
|
|
/// wäre das ein Vielfaches der nötigen Arbeit.
|
|
///
|
|
/// Meldet sich der letzte Konsument ab, hört die Schleife auf. Im Ruhezustand
|
|
/// misst Onyx nichts.
|
|
@MainActor
|
|
@Observable
|
|
public final class MetricsModel {
|
|
|
|
public private(set) var snapshot = MetricsSnapshot()
|
|
/// Verlauf für die Graphen, jüngster Wert zuletzt.
|
|
public private(set) var history: [MetricsSnapshot] = []
|
|
|
|
/// So viele Punkte zeigt ein Menüleisten-Graph. Mehr aufzuheben kostet
|
|
/// Speicher für etwas, das niemand sieht.
|
|
public static let historyLength = 60
|
|
|
|
private let source = SystemMetrics()
|
|
private var timer: Timer?
|
|
/// Je Konsument das gewünschte Intervall. Gemessen wird mit dem kürzesten.
|
|
private var demands: [UUID: TimeInterval] = [:]
|
|
|
|
public init() {}
|
|
|
|
/// Meldet Bedarf an. Der Rückgabewert wird zum Abmelden gebraucht.
|
|
@discardableResult
|
|
public func addConsumer(interval: TimeInterval = 2) -> UUID {
|
|
let token = UUID()
|
|
demands[token] = min(max(interval, 1), 10)
|
|
restartTimer()
|
|
return token
|
|
}
|
|
|
|
public func removeConsumer(_ token: UUID) {
|
|
demands[token] = nil
|
|
restartTimer()
|
|
}
|
|
|
|
/// Ändert das Intervall eines bestehenden Konsumenten, ohne die Schleife
|
|
/// anzuhalten — etwa wenn das Panel aufgeht und schneller messen will.
|
|
public func updateConsumer(_ token: UUID, interval: TimeInterval) {
|
|
guard demands[token] != nil else { return }
|
|
demands[token] = min(max(interval, 1), 10)
|
|
restartTimer()
|
|
}
|
|
|
|
private func restartTimer() {
|
|
timer?.invalidate()
|
|
timer = nil
|
|
|
|
guard let interval = demands.values.min() else {
|
|
history.removeAll()
|
|
return
|
|
}
|
|
|
|
sample()
|
|
let timer = Timer(timeInterval: interval, repeats: true) { _ in
|
|
MainActor.assumeIsolated { [weak self] in self?.sample() }
|
|
}
|
|
// `.common`, sonst steht die Messung, während ein Menü offen ist.
|
|
RunLoop.main.add(timer, forMode: .common)
|
|
self.timer = timer
|
|
}
|
|
|
|
private func sample() {
|
|
let value = source.sample()
|
|
snapshot = value
|
|
history.append(value)
|
|
if history.count > Self.historyLength { history.removeFirst(history.count - Self.historyLength) }
|
|
}
|
|
}
|
|
|
|
public extension MetricsModel {
|
|
/// Verlauf einer einzelnen Größe, für die Mini-Graphen.
|
|
func series(_ metric: MetricKind) -> [Double] {
|
|
history.compactMap { snapshot in
|
|
switch metric {
|
|
case .cpu: snapshot.cpu.total
|
|
case .gpu: snapshot.gpu
|
|
case .memory: snapshot.memory.usedFraction
|
|
case .battery: snapshot.battery?.charge
|
|
case .temperature: snapshot.sensors.map(\.celsius).max().map { $0 / 100 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum MetricKind: String, CaseIterable, Sendable, Identifiable {
|
|
case cpu, gpu, memory, battery, temperature
|
|
public var id: String { rawValue }
|
|
|
|
public var symbolName: String {
|
|
switch self {
|
|
case .cpu: "cpu"
|
|
case .gpu: "cpu.fill"
|
|
case .memory: "memorychip"
|
|
case .battery: "battery.100"
|
|
case .temperature: "thermometer"
|
|
}
|
|
}
|
|
}
|