Files

170 lines
5.8 KiB
Swift

import Foundation
import SMC
/// 描述当前界面需要哪些指标。面板关闭时只采菜单栏用到的项目。
public struct MetricsDemand: Sendable, Equatable {
public var interval: Duration = .seconds(2)
public var memory = false
public var network = false
public var gpu = false
public var disk = false
public var battery = false
public var processes = false
/// 进程管理器打开时:连同其他用户与系统的进程一起采集
public var systemProcesses = false
public var temperatures: Set<TemperatureGroup> = []
public var fans = false
/// 整机功耗与 GPU 功耗
public var power = false
/// 各类核心的频率
public var cpuFrequency = false
/// 磁盘读写速率与 SMART 健康信息
public var diskDetail = false
public init() {}
}
public actor MetricsHub {
private var cpu = CPUSampler()
private let memory = MemorySampler()
private var network = NetworkSampler()
private var processes = ProcessSampler()
private let sensors = SensorSampler()
private let power = PowerSampler()
private var diskActivity = DiskActivitySampler()
private var demand = MetricsDemand()
private var handler: (@MainActor @Sendable (MetricsSnapshot) -> Void)?
private var loop: Task<Void, Never>?
private var paused = false
/// 启动后第一次采集全部指标,面板首次打开时各页面已有数据,高度一次测准
private var primed = false
private var lastDisk = Date.distantPast
private var lastBattery = Date.distantPast
private var lastInterface = Date.distantPast
private var lastPower = Date.distantPast
private var lastDiskHealth = Date.distantPast
public init() {}
public func start(handler: @escaping @MainActor @Sendable (MetricsSnapshot) -> Void) {
self.handler = handler
restart()
}
public func update(_ newDemand: MetricsDemand) {
guard newDemand != demand else { return }
let needsImmediateRefresh = newDemand.interval < demand.interval
|| (newDemand.processes && !demand.processes)
|| (newDemand.disk && !demand.disk)
|| (newDemand.diskDetail && !demand.diskDetail)
if (newDemand.power && !demand.power) || (newDemand.cpuFrequency && !demand.cpuFrequency) {
lastPower = .distantPast
}
demand = newDemand
if needsImmediateRefresh {
lastDisk = .distantPast
lastBattery = .distantPast
lastInterface = .distantPast
lastDiskHealth = .distantPast
}
restart()
}
/// 锁屏、屏幕休眠或系统睡眠时暂停采样
public func setPaused(_ value: Bool) {
guard value != paused else { return }
paused = value
restart()
}
public func stop() {
loop?.cancel()
loop = nil
handler = nil
}
private static func everything(interval: Duration) -> MetricsDemand {
var demand = MetricsDemand()
demand.interval = interval
demand.memory = true
demand.network = true
demand.gpu = true
demand.disk = true
demand.battery = true
demand.processes = true
demand.temperatures = Set(TemperatureGroup.allCases)
demand.fans = true
demand.power = true
demand.cpuFrequency = true
demand.diskDetail = true
return demand
}
private func restart() {
loop?.cancel()
guard !paused, handler != nil else {
loop = nil
return
}
loop = Task { await self.run() }
}
private func run() async {
while !Task.isCancelled {
let snapshot = collect()
if let handler { await handler(snapshot) }
// 容差让系统合并定时器唤醒,降低功耗
try? await Task.sleep(for: demand.interval, tolerance: demand.interval / 4)
}
}
private func collect() -> MetricsSnapshot {
let now = Date()
var snapshot = MetricsSnapshot()
let demand = primed ? self.demand : Self.everything(interval: self.demand.interval)
primed = true
snapshot.cpu = cpu.sample()
if demand.memory { snapshot.memory = memory.sample() }
if demand.network {
snapshot.network = network.sample()
if now.timeIntervalSince(lastInterface) > 15 {
snapshot.networkInterface = NetworkSampler.primaryInterface()
lastInterface = now
}
}
if demand.gpu { snapshot.gpu = GPUSampler.sample() }
if demand.disk, now.timeIntervalSince(lastDisk) > 30 {
snapshot.disk = DiskSampler.sample()
lastDisk = now
}
if demand.battery, now.timeIntervalSince(lastBattery) > 10 {
snapshot.battery = BatterySampler.sample()
lastBattery = now
}
if demand.processes || demand.systemProcesses {
snapshot.processes = processes.sample(includeSystem: demand.systemProcesses)
snapshot.systemCounts = SystemCounts.read()
}
if !demand.temperatures.isEmpty || demand.fans {
snapshot.sensors = sensors.sample(groups: demand.temperatures, includeFans: demand.fans)
}
if demand.diskDetail {
snapshot.diskActivity = diskActivity.sample(now: now)
if now.timeIntervalSince(lastDiskHealth) > 60 {
snapshot.diskHealth = DiskHealthReader.read()
lastDiskHealth = now
}
}
// IOReport 采样有一定开销,至少间隔 2 秒
if demand.power || demand.cpuFrequency, now.timeIntervalSince(lastPower) >= 1.9 {
snapshot.power = power.sample(gpu: demand.power, frequency: demand.cpuFrequency, now: now)
lastPower = now
}
return snapshot
}
}