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" } } }