Files
onyx/Packages/OnyxKit/Sources/MetricsProvider/Calculations.swift
Scarriffle 57093cb63b Menüleiste: Prozentzeichen ohne Leerzeichen
Die Breite ist fest für „100 %" reserviert, damit nichts springt, wenn der
Wert eine Stelle mehr bekommt. Das Leerzeichen kostet in dieser Reservierung
drei Punkte je Element — in der Menüleiste ist Platz das knappste Gut.

Nur dort. Im Panel und in den Popovern bleibt das Leerzeichen stehen; da ist
Platz, und typografisch gehört es hin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 13:40:06 +02:00

126 lines
5.1 KiB
Swift

import Foundation
/// Die CPU-Zeitzähler eines Kerns, wie `host_processor_info` sie liefert.
///
/// Es sind **Summen seit dem Start**, keine Momentanwerte. Die Auslastung
/// ergibt sich erst aus der Differenz zweier Abtastungen.
public struct CPUTicks: Equatable, Sendable {
public let user: UInt32
public let system: UInt32
public let idle: UInt32
public let nice: UInt32
public init(user: UInt32, system: UInt32, idle: UInt32, nice: UInt32) {
self.user = user
self.system = system
self.idle = idle
self.nice = nice
}
/// Auslastung zwischen zwei Abtastungen, 0 bis 1.
public static func usage(from previous: CPUTicks, to current: CPUTicks) -> Double {
// `&-` statt `-`: die Zähler sind 32 Bit breit und laufen nach gut
// 400 Tagen über. Ein gewöhnlicher Abzug stürzt dabei ab oder liefert
// einen absurden Sprung; die überlaufende Subtraktion ergibt genau die
// richtige Differenz.
let user = UInt64(current.user &- previous.user)
let system = UInt64(current.system &- previous.system)
let nice = UInt64(current.nice &- previous.nice)
let idle = UInt64(current.idle &- previous.idle)
let busy = user + system + nice
let total = busy + idle
guard total > 0 else { return 0 }
return min(max(Double(busy) / Double(total), 0), 1)
}
}
/// Wie angespannt die Speicherlage ist.
public enum MemoryPressure: Equatable, Sendable {
case normal
case warning
case critical
/// - Parameters:
/// - free: Anteil wirklich freien Speichers, 0 bis 1.
/// - compressed: Anteil komprimierten Speichers, 0 bis 1.
/// - swapUsed: belegter Auslagerungsspeicher in Byte.
public static func classify(free: Double, compressed: Double, swapUsed: UInt64) -> MemoryPressure {
// Auslagerung schlägt alles andere: sobald sie läuft, bremst der
// Rechner spürbar auch wenn die Freispeicheranzeige harmlos aussieht.
// Ein bisschen Swap hat macOS fast immer angelegt, deshalb erst ab
// einem halben Gigabyte.
if swapUsed > 512 * 1024 * 1024 { return .critical }
if free < 0.10 || compressed > 0.20 { return .warning }
return .normal
}
}
/// Netzwerkdurchsatz aus zwei Zählerständen.
public enum Throughput {
/// Byte pro Sekunde zwischen zwei Abtastungen.
///
/// Die Normierung auf die tatsächlich verstrichene Zeit ist der Kern: die
/// Menüleisten-Module tasten je nach Einstellung alle 1 bis 10 Sekunden ab.
/// Ohne sie zeigte ein Modul mit 5 Sekunden Intervall das Fünffache an.
public static func rate(previous: UInt64, current: UInt64, elapsed: TimeInterval) -> Double {
guard elapsed > 0 else { return 0 }
// Zähler kleiner als vorher heißt: er wurde zurückgesetzt. Das passiert
// bei jedem Interface-Wechsel WLAN aus, VPN an und beim Neustart
// des Dienstes.
//
// Ein Überlauf wäre die andere denkbare Erklärung, kommt hier aber
// nicht vor: `if_data64` liefert 64-Bit-Zähler, die bei 10 Gbit/s
// erst nach rund 470 Jahren umlaufen. Der Versuch, beides zu
// unterscheiden, produziert nur den Ausreißer, den er verhindern soll
// deshalb null.
guard current >= previous else { return 0 }
return Double(current - previous) / elapsed
}
/// 2,4 MB/s" statt 2400000".
public static func formatted(_ bytesPerSecond: Double) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .binary
formatter.allowedUnits = [.useKB, .useMB, .useGB]
formatter.zeroPadsFractionDigits = false
// Ohne das steht bei null Zero KB/s" statt 0 KB/s" in einer Spalte
// aus Zahlen fällt ein Wort unangenehm auf und ändert die Breite.
formatter.allowsNonnumericFormatting = false
return formatter.string(fromByteCount: Int64(max(bytesPerSecond, 0))) + "/s"
}
}
/// Einheitliche Zahlendarstellung für alle Messwerte.
public enum MetricFormat {
/// Ganzzahlig. Nachkommastellen bei der Auslastung täuschen eine
/// Genauigkeit vor, die die Messung nicht hat und sie zappeln im
/// Sekundentakt, was die Anzeige unruhig macht.
public static func percent(_ fraction: Double) -> String {
"\(Int((min(max(fraction, 0), 1) * 100).rounded())) %"
}
/// Ohne Leerzeichen nur für die Menüleiste.
///
/// Dort ist die Breite fest für 100 %" reserviert, damit nichts springt,
/// wenn der Wert eine Stelle mehr bekommt. Das Leerzeichen kostet in dieser
/// Reservierung drei Punkte je Element, und in der Menüleiste ist Platz
/// das knappste Gut. Im Panel und in den Popovern bleibt es stehen.
public static func percentCompact(_ fraction: Double) -> String {
"\(Int((min(max(fraction, 0), 1) * 100).rounded()))%"
}
public static func temperature(_ celsius: Double) -> String {
"\(Int(celsius.rounded()))°"
}
public static func watts(_ value: Double) -> String {
String(format: "%.1f W", max(value, 0))
}
}