- 账号与同步:GitHub / Google / Apple 登录后把菜单栏项目与风格等偏好同步到 getopenstats.com,换一台 Mac 登录即恢复(AccountSync 模块、SyncController、SettingsDocument、账号设置页,openstats:// 回调) - 磁盘页成为工具页:已用 / 可清除 / 可用分段容量条、空间分析、文件系统检查、本地快照清理(辅助工具协议 v4)、其他磁盘推出 - 公网 IP:归属地数据源可选 CleanIP.io / ipapi.is / DB-IP / ipinfo.io / 本地 GeoLite2,每台 Mac 自己查、本地缓存 7 天;IPv4 / IPv6 双栈各自查询与切换;弹窗显示 ASN、运营方、原生 / 住宅等徽章与纯净度评分色带;接口区块可重置上传 / 下载统计 - 弹窗:右上角按钮改为该指标自己的图标 + 总设置;主窗口页面标题栏的开关点不动已修复;滚动条改为悬浮式,内容不再偏左;网络弹窗卡片被标题行撑宽的问题已修复 - 设置:⌘ 拖动排序说明;每个菜单栏项目单独选风格并带实时预览;网络与磁盘的进程榜单固定 5 行并平滑排序;CPU 持续高负载通知 - 设计系统:分段条、评分色带、标签徽章、胶囊切换、刷新按钮等组件与徽章配色 token - 更新日志、架构说明、README 与译文同步
134 lines
6.0 KiB
Swift
134 lines
6.0 KiB
Swift
import Foundation
|
|
import Metrics
|
|
import Observation
|
|
import SMC
|
|
|
|
/// 一次采样的时间与数值
|
|
public struct TimedValue: Sendable, Equatable {
|
|
public let date: Date
|
|
public let value: Double
|
|
|
|
public init(date: Date, value: Double) {
|
|
self.date = date
|
|
self.value = value
|
|
}
|
|
}
|
|
|
|
/// 主线程上的最新指标与短历史,只保留界面需要的数据
|
|
@MainActor
|
|
@Observable
|
|
public final class MetricsStore {
|
|
public static let historyCapacity = 60
|
|
|
|
public let topology = CPUSampler.topology()
|
|
public private(set) var system = SystemInfoReader.read()
|
|
|
|
public private(set) var cpu: CPULoad?
|
|
public private(set) var cpuTotal = History<Double>(capacity: historyCapacity)
|
|
/// 带时间戳的 CPU 总占用,供可选 1 / 3 / 5 分钟的走势图按时间定位;每秒一次时正好装下 5 分钟
|
|
public private(set) var cpuTimeline = History<TimedValue>(capacity: timelineCapacity)
|
|
public static let timelineCapacity = 300
|
|
/// 每次采样各核心的占用,用于核心热力图
|
|
public private(set) var coreHistory = History<[Double]>(capacity: historyCapacity)
|
|
|
|
public private(set) var memory: MemoryUsage?
|
|
public private(set) var memoryHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var pressureHistory = History<MemoryPressure>(capacity: historyCapacity)
|
|
/// 交换区换入 / 换出速率(字节 / 秒)
|
|
public private(set) var swapRate: (swapIn: Double, swapOut: Double)?
|
|
@ObservationIgnored private var lastSwap: (date: Date, swapIn: UInt64, swapOut: UInt64)?
|
|
public private(set) var network: NetworkRate?
|
|
public private(set) var downloadHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var uploadHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var networkInterface: NetworkInterfaceInfo?
|
|
|
|
public private(set) var gpu: GPUUsage?
|
|
public private(set) var gpuHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var disk: DiskUsage?
|
|
/// 启动时读取一次,确保面板首次打开时布局(是否有电池卡片)就已确定
|
|
public private(set) var battery: BatteryStatus? = BatterySampler.sample()
|
|
public private(set) var processes: [ProcessUsage] = []
|
|
/// 按应用汇总的磁盘读写排行,分数是最近十几秒的平均速率
|
|
private(set) var diskRanking = ActivityRanking()
|
|
public private(set) var systemCounts: SystemCounts?
|
|
public private(set) var sensors: SensorReadings?
|
|
public private(set) var power: PowerReading?
|
|
public private(set) var powerHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var diskActivity: DiskActivity?
|
|
public private(set) var diskReadHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var diskWriteHistory = History<Double>(capacity: historyCapacity)
|
|
public private(set) var diskHealth: DiskHealth?
|
|
public private(set) var lastUpdate: Date?
|
|
|
|
public init() {}
|
|
|
|
public func apply(_ snapshot: MetricsSnapshot) {
|
|
lastUpdate = snapshot.date
|
|
if let activity = snapshot.diskActivity {
|
|
diskActivity = activity
|
|
diskReadHistory.append(activity.readRate)
|
|
diskWriteHistory.append(activity.writeRate)
|
|
}
|
|
if let health = snapshot.diskHealth { diskHealth = health }
|
|
if let power = snapshot.power {
|
|
self.power = power
|
|
if let system = power.system { powerHistory.append(system) }
|
|
}
|
|
if let cpu = snapshot.cpu {
|
|
self.cpu = cpu
|
|
cpuTotal.append(cpu.total)
|
|
cpuTimeline.append(TimedValue(date: snapshot.date, value: cpu.total))
|
|
coreHistory.append(cpu.perCore)
|
|
}
|
|
if let memory = snapshot.memory {
|
|
self.memory = memory
|
|
memoryHistory.append(memory.usedFraction)
|
|
pressureHistory.append(memory.pressure)
|
|
if let last = lastSwap, snapshot.date > last.date {
|
|
let seconds = snapshot.date.timeIntervalSince(last.date)
|
|
swapRate = (Double(memory.swapInBytes >= last.swapIn ? memory.swapInBytes - last.swapIn : 0) / seconds,
|
|
Double(memory.swapOutBytes >= last.swapOut ? memory.swapOutBytes - last.swapOut : 0) / seconds)
|
|
}
|
|
lastSwap = (snapshot.date, memory.swapInBytes, memory.swapOutBytes)
|
|
}
|
|
if let network = snapshot.network {
|
|
self.network = network
|
|
downloadHistory.append(network.downloadBytesPerSecond)
|
|
uploadHistory.append(network.uploadBytesPerSecond)
|
|
}
|
|
if let interface = snapshot.networkInterface { networkInterface = interface }
|
|
if let gpu = snapshot.gpu {
|
|
self.gpu = gpu
|
|
gpuHistory.append(gpu.utilization)
|
|
}
|
|
if let disk = snapshot.disk { self.disk = disk }
|
|
if let battery = snapshot.battery { self.battery = battery }
|
|
if let processes = snapshot.processes {
|
|
self.processes = processes
|
|
var activity: [String: Double] = [:]
|
|
for row in ProcessRowModel.grouped(processes) {
|
|
activity[row.id] = row.disk.map { $0.read + $0.write } ?? 0
|
|
}
|
|
diskRanking.update(activity)
|
|
}
|
|
if let counts = snapshot.systemCounts { systemCounts = counts }
|
|
if let sensors = snapshot.sensors { mergeSensors(sensors) }
|
|
}
|
|
|
|
/// 不同页面采集的温度分组不同,按组合并,避免切页时数据闪烁
|
|
private func mergeSensors(_ new: SensorReadings) {
|
|
guard let old = sensors else {
|
|
sensors = new
|
|
return
|
|
}
|
|
var byGroup = Dictionary(uniqueKeysWithValues: old.temperatures.map { ($0.group, $0) })
|
|
for summary in new.temperatures { byGroup[summary.group] = summary }
|
|
let ordered = TemperatureGroup.allCases.compactMap { byGroup[$0] }
|
|
sensors = SensorReadings(temperatures: ordered, fans: new.fans.isEmpty ? old.fans : new.fans)
|
|
}
|
|
|
|
var fastestFan: FanState? {
|
|
sensors?.fans.max { $0.current < $1.current }
|
|
}
|
|
}
|