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:
@@ -30,7 +30,8 @@ public final class MetricMenuBarModule: MenuBarModule {
|
||||
|
||||
public func makeStatusView(presentation: MenuBarPresentation) -> NSView {
|
||||
let view = MetricStatusView(metric: metric, presentation: presentation)
|
||||
view.update(snapshot: model.snapshot, history: model.series(metric))
|
||||
view.update(snapshot: model.snapshot, history: model.series(metric),
|
||||
mode: model.displayMode(for: metric))
|
||||
self.view = view
|
||||
return view
|
||||
}
|
||||
@@ -57,10 +58,12 @@ public final class MetricMenuBarModule: MenuBarModule {
|
||||
private func startObserving() {
|
||||
withObservationTracking {
|
||||
_ = model.snapshot
|
||||
_ = model.displayModes // auch auf den Moduswechsel reagieren
|
||||
} onChange: {
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, token != nil else { return }
|
||||
view?.update(snapshot: model.snapshot, history: model.series(metric))
|
||||
view?.update(snapshot: model.snapshot, history: model.series(metric),
|
||||
mode: model.displayMode(for: metric))
|
||||
startObserving()
|
||||
}
|
||||
}
|
||||
@@ -79,6 +82,7 @@ final class MetricStatusView: NSView {
|
||||
private let presentation: MenuBarPresentation
|
||||
private var snapshot = MetricsSnapshot()
|
||||
private var history: [Double] = []
|
||||
private var mode: MetricDisplayMode = .usage
|
||||
|
||||
init(metric: MetricKind, presentation: MenuBarPresentation) {
|
||||
self.metric = metric
|
||||
@@ -93,9 +97,10 @@ final class MetricStatusView: NSView {
|
||||
NSSize(width: presentation.width, height: 22)
|
||||
}
|
||||
|
||||
func update(snapshot: MetricsSnapshot, history: [Double]) {
|
||||
func update(snapshot: MetricsSnapshot, history: [Double], mode: MetricDisplayMode) {
|
||||
self.snapshot = snapshot
|
||||
self.history = history
|
||||
self.mode = mode
|
||||
needsDisplay = true
|
||||
}
|
||||
|
||||
@@ -106,7 +111,7 @@ final class MetricStatusView: NSView {
|
||||
|
||||
switch presentation {
|
||||
case .value:
|
||||
drawText(MetricSummary.value(metric, snapshot), color: color)
|
||||
drawText(MetricSummary.value(metric, snapshot, mode: mode), color: color)
|
||||
case .symbol:
|
||||
drawSymbol(color: color)
|
||||
case .graph:
|
||||
@@ -115,7 +120,7 @@ final class MetricStatusView: NSView {
|
||||
drawBars(color: color)
|
||||
case .valueAndGraph:
|
||||
let split = bounds.width * 0.55
|
||||
drawText(MetricSummary.value(metric, snapshot), color: color,
|
||||
drawText(MetricSummary.value(metric, snapshot, mode: mode), color: color,
|
||||
in: NSRect(x: 0, y: 0, width: split, height: bounds.height))
|
||||
drawGraph(color: color,
|
||||
in: NSRect(x: split + 2, y: 5,
|
||||
@@ -166,8 +171,11 @@ final class MetricStatusView: NSView {
|
||||
/// sondern ein Muster — aber genau daran erkennt man auf einen Blick, ob
|
||||
/// eine einzelne Last läuft oder alles ausgelastet ist.
|
||||
private func drawBars(color: NSColor) {
|
||||
let values = metric == .cpu ? snapshot.cpu.perCore
|
||||
: [MetricSummary.fraction(metric, snapshot)]
|
||||
// Bei der CPU-Auslastung ein Balken je Kern; sonst — auch bei der
|
||||
// CPU-Temperatur — ein einzelner Balken.
|
||||
let values = (metric == .cpu && mode == .usage)
|
||||
? snapshot.cpu.perCore
|
||||
: [MetricSummary.fraction(metric, snapshot, mode: mode)]
|
||||
guard !values.isEmpty, let context = NSGraphicsContext.current?.cgContext else { return }
|
||||
|
||||
let area = bounds.insetBy(dx: 2, dy: 5)
|
||||
@@ -253,6 +261,42 @@ private struct MetricPopover: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if !model.snapshot.fans.isEmpty {
|
||||
Divider()
|
||||
ForEach(model.snapshot.fans) { fan in
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
Label("Lüfter \(fan.index + 1)", systemImage: "fan")
|
||||
.font(.callout)
|
||||
Spacer()
|
||||
Text("\(Int(fan.rpm)) U/min")
|
||||
.font(.callout.monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
// Der Balken zeigt die Drehzahl zwischen Minimum und
|
||||
// Maximum der Firmware — „2321 U/min" allein sagt
|
||||
// niemandem, ob das viel ist.
|
||||
ProgressView(value: fan.fraction).controlSize(.small)
|
||||
}
|
||||
}
|
||||
Text("metric.fan.controlComing", bundle: .module)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if metric.supportsDisplayModes {
|
||||
Divider()
|
||||
Picker("", selection: Binding(
|
||||
get: { model.displayMode(for: metric) },
|
||||
set: { model.setDisplayMode($0, for: metric) })) {
|
||||
Text("metric.mode.usage", bundle: .module).tag(MetricDisplayMode.usage)
|
||||
Text("metric.mode.temperature", bundle: .module)
|
||||
.tag(MetricDisplayMode.temperature)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
.padding(14)
|
||||
|
||||
Reference in New Issue
Block a user