Phase 5: Hardware- und Netzwerkmonitor, Panel und Menüleiste
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.
This commit is contained in:
108
Packages/OnyxKit/Sources/MetricsProvider/MetricsModel.swift
Normal file
108
Packages/OnyxKit/Sources/MetricsProvider/MetricsModel.swift
Normal file
@@ -0,0 +1,108 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user