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:
Guido Schmit
2026-08-10 22:05:38 +02:00
parent 594d7c422a
commit a4cc75ab6e
10 changed files with 914 additions and 520 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -258,7 +258,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private func showSettings(tab: SettingsTab) {
guard let model, let calendarModel, let weatherModel else { return }
settingsWindow.show(model: model, calendarModel: calendarModel,
weatherModel: weatherModel, tab: tab)
weatherModel: weatherModel, metricsModel: metricsModel, tab: tab)
}
@objc private func quit() { NSApp.terminate(nil) }

View File

@@ -4,6 +4,7 @@ import OnyxNotch
import OnyxWidgetKit
import CalendarProvider
import OnyxMenuBar
import MetricsProvider
import WeatherProvider
enum SettingsTab: Hashable {
@@ -14,6 +15,7 @@ struct SettingsView: View {
@Bindable var model: AppModel
let calendarModel: CalendarModel
let weatherModel: WeatherModel
let metricsModel: MetricsModel?
@Binding var selectedTab: SettingsTab
var body: some View {
@@ -24,7 +26,7 @@ struct SettingsView: View {
DisplaySettings(model: model)
.tabItem { Label("settings.tab.display", systemImage: "macbook") }
.tag(SettingsTab.display)
MenuBarSettings(model: model)
MenuBarSettings(model: model, metricsModel: metricsModel)
.tabItem { Label("settings.tab.menubar", systemImage: "menubar.rectangle") }
.tag(SettingsTab.menubar)
PermissionSettings(calendarModel: calendarModel, weatherModel: weatherModel)
@@ -160,6 +162,7 @@ private struct PermissionRow: View {
/// blockiert die Kamera zusätzlich die Mitte.
private struct MenuBarSettings: View {
@Bindable var model: AppModel
let metricsModel: MetricsModel?
private static let modules: [(id: String, label: LocalizedStringKey, symbol: String)] = [
("metric.cpu", "metric.cpu", "cpu"),
@@ -180,7 +183,8 @@ private struct MenuBarSettings: View {
List {
ForEach(Self.modules, id: \.id) { module in
ModuleRow(model: model, id: module.id,
label: module.label, symbol: module.symbol)
label: module.label, symbol: module.symbol,
metricsModel: metricsModel)
}
}
.listStyle(.inset)
@@ -197,6 +201,13 @@ private struct ModuleRow: View {
let id: String
let label: LocalizedStringKey
let symbol: String
let metricsModel: MetricsModel?
/// Zu welcher Hardwaregröße dieses Modul gehört `nil` beim Netzwerk.
private var metricKind: MetricKind? {
guard id.hasPrefix("metric.") else { return nil }
return MetricKind(rawValue: String(id.dropFirst("metric.".count)))
}
var body: some View {
let settings = model.menuBarSettings(for: id)
@@ -214,6 +225,21 @@ private struct ModuleRow: View {
Spacer()
// Bei CPU und GPU zusätzlich wählbar, was gezeigt wird. Beide Werte
// stammen aus derselben Messung ein zweites Element in der
// ohnehin knappen Menüleiste wäre Verschwendung.
if let metric = metricKind, metric.supportsDisplayModes, let metricsModel {
Picker("", selection: Binding(
get: { metricsModel.displayMode(for: metric) },
set: { metricsModel.setDisplayMode($0, for: metric) })) {
Text("metric.mode.usage").tag(MetricDisplayMode.usage)
Text("metric.mode.temperature").tag(MetricDisplayMode.temperature)
}
.labelsHidden()
.frame(width: 110)
.disabled(!settings.isEnabled)
}
Picker("", selection: Binding(
get: { settings.presentation },
set: { presentation in

View File

@@ -2,6 +2,7 @@ import AppKit
import SwiftUI
import CalendarProvider
import WeatherProvider
import MetricsProvider
/// Führt das Einstellungsfenster selbst.
///
@@ -25,7 +26,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
var keepsDockIcon = false
func show(model: AppModel, calendarModel: CalendarModel, weatherModel: WeatherModel,
tab: SettingsTab = .widgets) {
metricsModel: MetricsModel?, tab: SettingsTab = .widgets) {
selectedTab = tab
keepsDockIcon = model.showsDockIcon
@@ -42,6 +43,7 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
rootView: SettingsView(model: model,
calendarModel: calendarModel,
weatherModel: weatherModel,
metricsModel: metricsModel,
selectedTab: Binding(
get: { [weak self] in self?.selectedTab ?? .widgets },
set: { [weak self] in self?.selectedTab = $0 })))