CPU und GPU zwischen Auslastung und Temperatur umschaltbar
Die M-Serie-Konvention gilt auch auf dem M5: Tp* sind die CPU-Kerne, Tg* die GPU-Cluster. Gezeigt wird der wärmste Punkt, nicht der Durchschnitt — gedrosselt wird nach dem heißesten Kern. Gemessen: CPU 76 °C, GPU 70 °C. Die Fühlerliste wird zur Laufzeit gesucht statt fest verdrahtet: welche es gibt, hängt am Modell. Auf diesem Mac sind es 285 Temperaturfühler. Zwei Leistungsprobleme, gefunden bevor sie jemandem auffallen konnten: Die Suche geht alle 3486 Keys durch und braucht knapp eine Sekunde. Auf dem Hauptthread wäre das eine spürbare Startverzögerung — sie läuft jetzt im Hintergrund, bis dahin zeigt die Oberfläche "—" statt einer erfundenen Zahl. Fünfzig Fühler zu lesen kostete 34 ms je Abtastung, bei 1 Hz also 3,4 % CPU. Zwei Maßnahmen: Typ und Größe je Key werden zwischengespeichert (sie stehen in der Firmware fest, sie bei jedem Lesen zu erfragen verdoppelte die IOKit-Aufrufe), und Temperaturen werden höchstens alle zwei Sekunden neu gelesen — sie bewegen sich in einer Sekunde ohnehin kaum. Ergebnis: 6 ms Grundlast, 18 ms alle zwei Sekunden, im Mittel gut 1 %. Dabei noch eine Eigenheit gefunden: die Byte-Reihenfolge im SMC ist nicht durchgängig. Messwerte wie B0AV kommen little-endian aus dem Akku-Baustein, die Metadaten des SMC selbst dagegen big-endian. #KEY little-endian gelesen ergibt 2 651 652 096 statt 3486 — die Fühlersuche brach damit still ab und alle Temperaturen blieben leer. Lüfterdrehzahlen stehen jetzt im Sensor-Popover, mit Balken zwischen Minimum und Maximum der Firmware: "2321 U/min" allein sagt niemandem, ob das viel ist. Die Steuerung selbst kommt mit Phase 6 — Schreibzugriff auf den SMC verlangt root — und der Popover sagt das auch. 145 Tests grün.
This commit is contained in:
@@ -22,12 +22,37 @@ public final class MetricsModel {
|
||||
/// Speicher für etwas, das niemand sieht.
|
||||
public static let historyLength = 60
|
||||
|
||||
/// Was CPU und GPU zeigen. Beobachtbar, damit Panel und Menüleiste sofort
|
||||
/// umschalten.
|
||||
public private(set) var displayModes: [MetricKind: MetricDisplayMode] = [:]
|
||||
|
||||
private let defaults: UserDefaults
|
||||
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() {}
|
||||
public init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
for metric in MetricKind.allCases where metric.supportsDisplayModes {
|
||||
let stored = defaults.string(forKey: Self.key(metric))
|
||||
displayModes[metric] = stored.flatMap(MetricDisplayMode.init(rawValue:)) ?? .usage
|
||||
}
|
||||
}
|
||||
|
||||
public func displayMode(for metric: MetricKind) -> MetricDisplayMode {
|
||||
displayModes[metric] ?? .usage
|
||||
}
|
||||
|
||||
public func setDisplayMode(_ mode: MetricDisplayMode, for metric: MetricKind) {
|
||||
guard metric.supportsDisplayModes else { return }
|
||||
displayModes[metric] = mode
|
||||
defaults.set(mode.rawValue, forKey: Self.key(metric))
|
||||
}
|
||||
|
||||
private static func key(_ metric: MetricKind) -> String {
|
||||
"onyx.metric.\(metric.rawValue).displayMode"
|
||||
}
|
||||
|
||||
/// Meldet Bedarf an. Der Rückgabewert wird zum Abmelden gebraucht.
|
||||
@discardableResult
|
||||
@@ -80,18 +105,33 @@ public final class MetricsModel {
|
||||
public extension MetricsModel {
|
||||
/// Verlauf einer einzelnen Größe, für die Mini-Graphen.
|
||||
func series(_ metric: MetricKind) -> [Double] {
|
||||
history.compactMap { snapshot in
|
||||
let mode = displayMode(for: metric)
|
||||
return history.compactMap { snapshot in
|
||||
if mode == .temperature, metric.supportsDisplayModes {
|
||||
let celsius = metric == .cpu ? snapshot.cpu.temperature : snapshot.gpuTemperature
|
||||
return celsius.map { $0 / 100 }
|
||||
}
|
||||
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 }
|
||||
case .cpu: return snapshot.cpu.total
|
||||
case .gpu: return snapshot.gpu
|
||||
case .memory: return snapshot.memory.usedFraction
|
||||
case .battery: return snapshot.battery?.charge
|
||||
case .temperature: return snapshot.sensors.map(\.celsius).max().map { $0 / 100 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Was ein CPU- oder GPU-Element zeigt.
|
||||
///
|
||||
/// Beide Werte kommen aus derselben Messung — die Umschaltung kostet nichts und
|
||||
/// erspart ein zweites Element in einer ohnehin knappen Menüleiste.
|
||||
public enum MetricDisplayMode: String, Codable, Sendable, CaseIterable, Identifiable {
|
||||
case usage
|
||||
case temperature
|
||||
public var id: String { rawValue }
|
||||
}
|
||||
|
||||
public enum MetricKind: String, CaseIterable, Sendable, Identifiable {
|
||||
case cpu, gpu, memory, battery, temperature
|
||||
public var id: String { rawValue }
|
||||
@@ -105,4 +145,8 @@ public enum MetricKind: String, CaseIterable, Sendable, Identifiable {
|
||||
case .temperature: "thermometer"
|
||||
}
|
||||
}
|
||||
|
||||
/// Nur CPU und GPU haben beides. Speicher und Akku haben keine eigene
|
||||
/// Temperatur, die Sensorkarte zeigt ohnehin nur Temperaturen.
|
||||
public var supportsDisplayModes: Bool { self == .cpu || self == .gpu }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user