Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

131 lines
5.1 KiB
Swift

// PR 15 — System metrics for the Status screen.
//
// The redesign's System block exposes live RAM usage, thermal state, and
// the GPU/utilization proxy alongside the existing uptime and version rows.
// We poll Mach (`host_statistics64`) every 5 seconds on-screen — cheap,
// kernel-resident, no entitlements required. `ProcessInfo.thermalState`
// changes infrequently but we re-read it on the same tick to avoid a
// second timer.
import Foundation
import Darwin.Mach
/// Bucketed thermal levels. Decoupled from the SwiftUI `Color` so the
/// mapping can be unit-tested without dragging in the theme.
enum ThermalSeverity: String, Equatable, Sendable {
case nominal, fair, serious, critical
}
@MainActor
@Observable
final class SystemMetricsPoller {
var ramUsedBytes: UInt64?
var ramTotalBytes: UInt64?
var thermalState: ProcessInfo.ThermalState = .nominal
@ObservationIgnored
private var pollTask: Task<Void, Never>?
func start() {
pollTask?.cancel()
// Read once immediately so the bar doesn't flash empty on appear.
tick()
pollTask = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(5))
if Task.isCancelled { return }
await MainActor.run { self?.tick() }
}
}
}
func stop() {
pollTask?.cancel()
pollTask = nil
}
private func tick() {
thermalState = ProcessInfo.processInfo.thermalState
ramTotalBytes = ProcessInfo.processInfo.physicalMemory
if let used = Self.readRamUsedBytes() {
ramUsedBytes = used
}
}
// MARK: - Mach
/// Reads (active + wired + compressor) page counts and multiplies by
/// the kernel page size. This is the closest analogue to Activity
/// Monitor's "Memory Used" without parsing memory_pressure or shelling
/// to `vm_stat`.
nonisolated private static func readRamUsedBytes() -> UInt64? {
var size = mach_msg_type_number_t(
MemoryLayout<vm_statistics64_data_t>.stride / MemoryLayout<integer_t>.stride
)
var stats = vm_statistics64_data_t()
let result = withUnsafeMutablePointer(to: &stats) { ptr -> kern_return_t in
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { rebound in
host_statistics64(mach_host_self(), HOST_VM_INFO64, rebound, &size)
}
}
guard result == KERN_SUCCESS else { return nil }
// Ask the kernel for the page size at runtime — the global
// `vm_kernel_page_size` is a mutable C var Swift can't import
// under strict concurrency, and `host_page_size` is a stable
// alternative shipped on every macOS we support.
var pageSize: vm_size_t = 0
guard host_page_size(mach_host_self(), &pageSize) == KERN_SUCCESS else {
return nil
}
let pages = UInt64(stats.active_count)
+ UInt64(stats.wire_count)
+ UInt64(stats.compressor_page_count)
return pages * UInt64(pageSize)
}
// MARK: - Helpers (exposed for tests + view layer)
/// Map the system's `ThermalState` enum (which has no stable raw
/// representation across SDKs) to our own severity. Anything Apple
/// adds beyond `.critical` later falls through to `.critical` — better
/// to flag too hot than to silently render as nominal.
nonisolated static func severity(for state: ProcessInfo.ThermalState) -> ThermalSeverity {
switch state {
case .nominal: return .nominal
case .fair: return .fair
case .serious: return .serious
case .critical: return .critical
@unknown default: return .critical
}
}
nonisolated static func label(for severity: ThermalSeverity) -> String {
switch severity {
case .nominal:
return String(localized: "metrics.thermal.nominal",
defaultValue: "Nominal",
comment: "Thermal severity label shown in the Status screen's System block")
case .fair:
return String(localized: "metrics.thermal.fair",
defaultValue: "Fair",
comment: "Thermal severity label shown in the Status screen's System block")
case .serious:
return String(localized: "metrics.thermal.serious",
defaultValue: "Serious",
comment: "Thermal severity label shown in the Status screen's System block")
case .critical:
return String(localized: "metrics.thermal.critical",
defaultValue: "Critical",
comment: "Thermal severity label shown in the Status screen's System block")
}
}
/// Format bytes as a 1-decimal GB string ("34.6"). Uses decimal GB
/// (10^9) to match how macOS reports system memory in About This Mac
/// — using GiB here would print 32 GB machines as "29.8".
nonisolated static func formatBytesAsGB(_ bytes: UInt64) -> String {
let gb = Double(bytes) / 1_000_000_000.0
return String(format: "%.1f", gb)
}
}