磁盘页:容量、读写速度、SSD 健康与读写最多的应用

This commit is contained in:
gentpan committed 2026-09-13 08:33:54 +05:00
1 parent 722b278832
commit 0b09fb91c1
12 files changed
+390 -5

No files matched your search

+1
View File
@@ -14,6 +14,7 @@
- 导出诊断信息(设置 · 关于):把版本、系统与辅助工具状态、主要设置、最近 3 天的运行日志、清理记录和最近的崩溃报告打包成 zip,反馈问题时附上;自动去掉用户目录名、IP 与硬件地址,不含序列号。应用与辅助工具的关键操作和错误写入系统日志。
- 系统通知(设置 · 通知):CPU 过热(持续 1 分钟高于设定温度)、内存压力严重(持续 30 秒)、磁盘空间不足(低于 10% 或 10 GB)、网络断开超过 20 秒(恢复后再提示)、电池最大容量低于 80%,逐项开关,默认关闭;同一状况持续期间只提醒一次并有冷却时间,点通知打开对应页面;可发送测试通知,通知被关闭时提示去系统设置打开。
- 功耗与频率:“温度与风扇”页和温度详情新增功耗,显示整机功耗走势、电源输入、电池充放电功率和 GPU 功耗(读 SMC 与系统能耗统计,不需要辅助工具);CPU 详情的核心分工显示各类核心工作时的平均频率。读取能耗统计有一定开销,只在这些界面打开时每 2 秒读一次。
- 磁盘页(主窗口侧边栏“磁盘”):启动磁盘容量、所有磁盘合计的实时读写速度与 60 秒走势、SSD 健康(通过系统自带的 NVMe SMART 接口读取剩余寿命、累计读写量、备用空间、温度、通电时间与次数、异常断电和介质错误,出现警告时提示备份)、读写磁盘最多的应用;磁盘空间不足的通知改为打开磁盘页。
#### 调整
@@ -0,0 +1,137 @@
import Foundation
import IOKit
/// 启动磁盘的 NVMe SMART 健康信息
public struct DiskHealth: Sendable, Equatable {
public var model: String
/// 已消耗的寿命(厂商估算,可以超过 100)
public var percentageUsed: Int
public var availableSpare: Int
public var availableSpareThreshold: Int
public var temperature: Double?
public var bytesRead: UInt64
public var bytesWritten: UInt64
public var powerOnHours: UInt64
public var powerCycles: UInt64
public var unsafeShutdowns: UInt64
public var mediaErrors: UInt64
public var criticalWarning: UInt8
public var remainingLife: Int { max(0, 100 - percentageUsed) }
}
/// 通过系统自带的 NVMeSMARTLib 插件读取 SMART,不需要 root
public enum DiskHealthReader {
public static func read() -> DiskHealth? {
var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IOBlockStorageDevice"), &iterator) == KERN_SUCCESS else {
return nil
}
defer { IOObjectRelease(iterator) }
while case let service = IOIteratorNext(iterator), service != 0 {
defer { IOObjectRelease(service) }
guard let capable = IORegistryEntryCreateCFProperty(service, "NVMe SMART Capable" as CFString, kCFAllocatorDefault, 0)?
.takeRetainedValue() as? Bool, capable,
let bytes = smartData(for: service) else { continue }
// 型号在上层的 NVMe 控制器上
let model = (IORegistryEntrySearchCFProperty(service, kIOServicePlane, "Model Number" as CFString, kCFAllocatorDefault,
IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) as? String)?
.trimmingCharacters(in: .whitespaces) ?? "SSD"
return parse(bytes, model: model)
}
return nil
}
/// NVMe 1.0c 5.10.1.2:512 字节 SMART / Health 日志,小端,无对齐填充
static func parse(_ data: [UInt8], model: String) -> DiskHealth? {
guard data.count >= 192 else { return nil }
func u64(_ offset: Int) -> UInt64 {
(0..<8).reduce(UInt64(0)) { $0 | UInt64(data[offset + $1]) << (8 * UInt64($1)) }
}
let kelvin = Double(UInt16(data[1]) | UInt16(data[2]) << 8)
// 数据单位为 1000 个 512 字节扇区
let unit: UInt64 = 512 * 1000
return DiskHealth(model: model,
percentageUsed: Int(data[5]),
availableSpare: Int(data[3]),
availableSpareThreshold: Int(data[4]),
temperature: kelvin > 200 ? kelvin - 273.15 : nil,
bytesRead: u64(32).multipliedReportingOverflow(by: unit).partialValue,
bytesWritten: u64(48).multipliedReportingOverflow(by: unit).partialValue,
powerOnHours: u64(128),
powerCycles: u64(112),
unsafeShutdowns: u64(144),
mediaErrors: u64(160),
criticalWarning: data[0])
}
private static func smartData(for service: io_service_t) -> [UInt8]? {
// kIOCFPlugInInterfaceID、kIONVMeSMARTUserClientTypeID、kIONVMeSMARTInterfaceID 是 C 宏,Swift 里按字节重建
let pluginInterfaceID = CFUUIDGetConstantUUIDWithBytes(nil, 0xC2, 0x44, 0xE8, 0x58, 0x10, 0x9C, 0x11, 0xD4,
0x91, 0xD4, 0x00, 0x50, 0xE4, 0xC6, 0x42, 0x6F)
let userClientTypeID = CFUUIDGetConstantUUIDWithBytes(nil, 0xAA, 0x0F, 0xA6, 0xF9, 0xC2, 0xD6, 0x45, 0x7F,
0xB1, 0x0B, 0x59, 0xA1, 0x32, 0x53, 0x29, 0x2F)
let smartInterfaceID = CFUUIDGetConstantUUIDWithBytes(nil, 0xCC, 0xD1, 0xDB, 0x19, 0xFD, 0x9A, 0x4D, 0xAF,
0xBF, 0x95, 0x12, 0x45, 0x4B, 0x23, 0x0A, 0xB6)
var plugin: UnsafeMutablePointer<UnsafeMutablePointer<IOCFPlugInInterface>?>?
var score: Int32 = 0
guard IOCreatePlugInInterfaceForService(service, userClientTypeID, pluginInterfaceID, &plugin, &score) == KERN_SUCCESS,
let plugin, let vtable = plugin.pointee?.pointee else { return nil }
defer { _ = vtable.Release(plugin) }
var smart: LPVOID?
guard vtable.QueryInterface(plugin, CFUUIDGetUUIDBytes(smartInterfaceID), &smart) == S_OK, let smart else { return nil }
// IONVMeSMARTInterface:IUNKNOWN_C_GUTS(4 个指针)、version、revision,之后第一个函数是 SMARTReadData
let interface = smart.assumingMemoryBound(to: UnsafeMutableRawPointer.self)
let table = interface.pointee
typealias Release = @convention(c) (UnsafeMutableRawPointer?) -> UInt32
typealias ReadData = @convention(c) (UnsafeMutableRawPointer?, UnsafeMutableRawPointer?) -> IOReturn
defer { _ = table.load(fromByteOffset: 24, as: Release.self)(smart) }
let readData = table.load(fromByteOffset: 40, as: ReadData.self)
var buffer = [UInt8](repeating: 0, count: 512)
let result = buffer.withUnsafeMutableBytes { readData(smart, $0.baseAddress) }
return result == kIOReturnSuccess ? buffer : nil
}
}
/// 所有块设备的读写速率(字节 / 秒)
public struct DiskActivity: Sendable, Equatable {
public var readRate: Double
public var writeRate: Double
}
/// 读取 IOBlockStorageDriver 的累计读写字节数,两次采样相减得到速率
public struct DiskActivitySampler {
private var previous: (date: Date, read: UInt64, write: UInt64)?
public init() {}
public mutating func sample(now: Date = Date()) -> DiskActivity? {
guard let totals = Self.totals() else { return nil }
defer { previous = (now, totals.read, totals.write) }
guard let previous, now > previous.date else { return nil }
let seconds = now.timeIntervalSince(previous.date)
// 磁盘被推出后累计值会变小,这一次不计
guard totals.read >= previous.read, totals.write >= previous.write, seconds < 60 else { return nil }
return DiskActivity(readRate: Double(totals.read - previous.read) / seconds,
writeRate: Double(totals.write - previous.write) / seconds)
}
static func totals() -> (read: UInt64, write: UInt64)? {
var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(kIOMainPortDefault, IOServiceMatching("IOBlockStorageDriver"), &iterator) == KERN_SUCCESS else {
return nil
}
defer { IOObjectRelease(iterator) }
var read: UInt64 = 0
var write: UInt64 = 0
while case let driver = IOIteratorNext(iterator), driver != 0 {
defer { IOObjectRelease(driver) }
guard let statistics = IORegistryEntryCreateCFProperty(driver, "Statistics" as CFString, kCFAllocatorDefault, 0)?
.takeRetainedValue() as? [String: Any] else { continue }
read &+= (statistics["Bytes (Read)"] as? NSNumber)?.uint64Value ?? 0
write &+= (statistics["Bytes (Write)"] as? NSNumber)?.uint64Value ?? 0
}
return (read, write)
}
}
@@ -18,6 +18,8 @@ public struct MetricsDemand: Sendable, Equatable {
public var power = false
/// 各类核心的频率
public var cpuFrequency = false
/// 磁盘读写速率与 SMART 健康信息
public var diskDetail = false
public init() {}
}
@@ -29,6 +31,7 @@ public actor MetricsHub {
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)?
@@ -41,6 +44,7 @@ public actor MetricsHub {
private var lastBattery = Date.distantPast
private var lastInterface = Date.distantPast
private var lastPower = Date.distantPast
private var lastDiskHealth = Date.distantPast
public init() {}
@@ -54,6 +58,7 @@ public actor MetricsHub {
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
}
@@ -62,6 +67,7 @@ public actor MetricsHub {
lastDisk = .distantPast
lastBattery = .distantPast
lastInterface = .distantPast
lastDiskHealth = .distantPast
}
restart()
@@ -93,6 +99,7 @@ public actor MetricsHub {
demand.fans = true
demand.power = true
demand.cpuFrequency = true
demand.diskDetail = true
return demand
}
@@ -145,6 +152,13 @@ public actor MetricsHub {
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)
@@ -193,6 +193,8 @@ public struct MetricsSnapshot: Sendable {
public var systemCounts: SystemCounts?
public var sensors: SensorReadings?
public var power: PowerReading?
public var diskActivity: DiskActivity?
public var diskHealth: DiskHealth?
public init() {}
}
@@ -0,0 +1,189 @@
import Metrics
import SwiftUI
/// 磁盘:容量、实时读写、SSD 健康(SMART)与读写最多的应用
struct DiskPage: View {
@Environment(AppModel.self) private var model
var body: some View {
PageScroll {
WeightedRow(weights: [1, 1]) {
CapacityCard()
ActivityCard()
}
.fixedSize(horizontal: false, vertical: true)
HealthCard()
DiskProcessesCard()
}
}
}
private struct CapacityCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let disk = model.store.disk
Card {
CardHeader(icon: "internaldrive", title: disk?.volumeName ?? "启动磁盘",
detail: disk.map { "共 \(Format.bytes($0.total, base: .decimal))" } ?? "读取中")
HStack(alignment: .firstTextBaseline, spacing: DS.Space.s1) {
Text(verbatim: disk.map { "\(Int(($0.usedFraction * 100).rounded()))" } ?? "—")
.dsFont(.xxl, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
.monospacedDigit()
Text(verbatim: "%").dsFont(.sm).foregroundStyle(DS.Palette.textSecondary)
Spacer()
}
ProgressTrack(fraction: disk?.usedFraction ?? 0,
color: (disk?.usedFraction ?? 0) > 0.9 ? DS.Palette.warning : DS.Palette.primary)
if let disk {
InfoRow(label: "已用", text: Format.bytes(disk.used, base: .decimal))
InfoRow(label: "可用", text: Format.bytes(disk.available, base: .decimal))
Text("可用空间包含系统可以随时清除的缓存,与访达显示一致")
.dsFont(.xs)
.foregroundStyle(DS.Palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
}
}
}
private struct ActivityCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let store = model.store
let reads = store.diskReadHistory.elements
let writes = store.diskWriteHistory.elements
Card {
CardHeader(icon: "arrow.up.arrow.down", title: "读写速度", detail: "所有磁盘合计")
HStack(spacing: DS.Space.s4) {
rate("读取", store.diskActivity?.readRate, color: DS.NetworkPalette.download)
rate("写入", store.diskActivity?.writeRate, color: DS.NetworkPalette.upload)
Spacer()
}
// 上半写入、下半读取,与网络流量图同一套配色
MirroredRateChart(upload: writes, download: reads, height: DS.Size.chartHeight * 2)
InfoRow(label: "60 秒峰值") {
Text(verbatim: "读 \(Format.menuBarRate(reads.max() ?? 0)) · 写 \(Format.menuBarRate(writes.max() ?? 0))")
}
}
}
private func rate(_ label: String, _ value: Double?, color: NSColor) -> some View {
VStack(alignment: .leading, spacing: DS.Space.s1) {
HStack(spacing: DS.Space.s1) {
Circle().fill(Color(nsColor: color)).frame(width: DS.Space.s2, height: DS.Space.s2)
Text(label).dsFont(.xs).foregroundStyle(DS.Palette.textSecondary)
}
Text(verbatim: value.map(Format.menuBarRate) ?? "—")
.dsFont(.lg, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
.monospacedDigit()
}
}
}
private struct HealthCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let health = model.store.diskHealth
Card {
CardHeader(icon: "heart.text.square", title: "SSD 健康", detail: health?.model ?? "读取中")
if let health {
let tone = tone(health)
HStack(alignment: .center, spacing: DS.Space.s6) {
RingGauge(fraction: Double(health.remainingLife) / 100, color: tone.color, size: DS.Space.s16 + DS.Space.s6) {
VStack(spacing: 0) {
Text(verbatim: "\(health.remainingLife)%")
.dsFont(.base, weight: .semibold)
.foregroundStyle(DS.Palette.textPrimary)
.monospacedDigit()
Text("剩余寿命").dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
}
}
VStack(alignment: .leading, spacing: DS.Space.s1) {
StatusBadge(text: summary(health), tone: tone)
Text("寿命按厂商估算的已用比例计算;写入量越大消耗越快,日常使用通常可用很多年")
.dsFont(.xs)
.foregroundStyle(DS.Palette.textTertiary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 0)
}
WeightedRow(weights: [1, 1], spacing: DS.Space.s6) {
VStack(spacing: DS.Space.s2) {
InfoRow(label: "累计写入", text: Format.bytes(health.bytesWritten, base: .decimal))
InfoRow(label: "累计读取", text: Format.bytes(health.bytesRead, base: .decimal))
InfoRow(label: "备用空间", text: "\(health.availableSpare)%(阈值 \(health.availableSpareThreshold)%)")
if let temperature = health.temperature {
InfoRow(label: "温度", text: Format.temperature(temperature, fahrenheit: model.settings.useFahrenheit))
}
}
VStack(spacing: DS.Space.s2) {
InfoRow(label: "通电时间", text: "\(health.powerOnHours.formatted()) 小时")
InfoRow(label: "通电次数", text: health.powerCycles.formatted())
InfoRow(label: "异常断电", text: health.unsafeShutdowns.formatted())
InfoRow(label: "介质错误", text: health.mediaErrors.formatted())
}
}
} else {
Text("正在读取 SMART 信息;外置磁盘或不支持 NVMe SMART 的磁盘不显示")
.dsFont(.xs)
.foregroundStyle(DS.Palette.textTertiary)
}
}
}
private func tone(_ health: DiskHealth) -> Tone {
if health.criticalWarning != 0 || health.mediaErrors > 0 || health.availableSpare < health.availableSpareThreshold { return .error }
return health.remainingLife < 20 ? .warning : .success
}
private func summary(_ health: DiskHealth) -> String {
if health.criticalWarning != 0 { return "磁盘报告了严重警告,建议尽快备份" }
if health.mediaErrors > 0 { return "出现介质错误,建议备份" }
if health.availableSpare < health.availableSpareThreshold { return "备用空间低于阈值,建议备份" }
return health.remainingLife < 20 ? "寿命偏低,注意备份" : "状态良好"
}
}
private struct DiskProcessesCard: View {
@Environment(AppModel.self) private var model
var body: some View {
let rows = ProcessRowModel.grouped(model.store.processes)
.compactMap { row in row.disk.map { (row, $0.read + $0.write) } }
.filter { $0.1 > 0 }
.sorted { $0.1 > $1.1 }
.prefix(8)
let peak = rows.first?.1 ?? 1
Card {
CardHeader(icon: "list.bullet.rectangle", title: "读写最多的应用", detail: "只含当前用户的进程")
if rows.isEmpty {
Text("最近没有应用在读写磁盘").dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
}
ForEach(Array(rows), id: \.0.id) { row, total in
VStack(spacing: DS.Space.s1) {
HStack(spacing: DS.Space.s2) {
AppIconCache.shared.image(bundlePath: row.bundlePath)
.resizable()
.frame(width: DS.Size.iconInline, height: DS.Size.iconInline)
Text(verbatim: row.title)
.dsFont(.xs, weight: .medium)
.foregroundStyle(DS.Palette.textPrimary)
.lineLimit(1)
Spacer(minLength: DS.Space.s2)
Text(verbatim: "读 \(Format.menuBarRate(row.disk?.read ?? 0)) · 写 \(Format.menuBarRate(row.disk?.write ?? 0))")
.dsFont(.xs, weight: .medium)
.foregroundStyle(DS.Palette.textPrimary)
.monospacedDigit()
}
ProgressTrack(fraction: total / peak, height: DS.Space.s1)
}
}
}
}
}
@@ -28,6 +28,7 @@ public struct MainWindowView: View {
case .cpu: DetailPage { CPUPopover() }
case .gpu: DetailPage { GPUPopover() }
case .memory: DetailPage { MemoryPopover() }
case .disk: DiskPage()
case .network:
DetailPage {
NetworkPopover()
@@ -31,6 +31,7 @@ enum SnapshotRenderer {
demand.fans = true
demand.power = true
demand.cpuFrequency = true
demand.diskDetail = true
await model.hub.update(demand)
await model.hub.start { snapshot in model.handle(snapshot) }
try? await Task.sleep(for: .seconds(12))
@@ -46,7 +46,7 @@ public enum AlertKind: String, CaseIterable, Identifiable, Sendable {
switch self {
case .cpuTemperature: .thermal
case .memoryPressure: .memory
case .diskSpace: .cleaner
case .diskSpace: .disk
case .networkDown: .network
case .batteryHealth: .system
}
@@ -65,9 +65,10 @@ public final class AppModel {
demand.network = true
let showing = { (page: PanelTab, item: MenuBarItem) in (window && tab == page) || popover == item }
demand.gpu = (window && [.overview, .system].contains(tab)) || menu.contains(.gpu) || showing(.gpu, .gpu)
demand.disk = window && [.overview, .system, .cleaner].contains(tab)
demand.disk = window && [.overview, .system, .cleaner, .disk].contains(tab)
demand.diskDetail = window && tab == .disk
demand.battery = (window && [.overview, .system, .keepAwake].contains(tab)) || keepAwake.lidClosedActive
demand.processes = (window && [.processes, .overview].contains(tab)) || showing(.cpu, .cpu) || showing(.memory, .memory)
demand.processes = (window && [.processes, .overview, .disk].contains(tab)) || showing(.cpu, .cpu) || showing(.memory, .memory)
demand.systemProcesses = window && tab == .processes
var groups = Set<TemperatureGroup>()
@@ -220,12 +220,12 @@ public enum AppearanceMode: String, CaseIterable, Identifiable, Sendable {
/// 主窗口侧边栏的页面
public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case overview, system, cpu, gpu, memory, network, thermal, processes, keepAwake, cleaner
case overview, system, cpu, gpu, memory, disk, network, thermal, processes, keepAwake, cleaner
case settingsGeneral, settingsMenuBar, settingsNotifications, settingsHelper, settingsAbout
public var id: String { rawValue }
static let monitors: [PanelTab] = [.overview, .system, .cpu, .gpu, .memory, .network, .thermal]
static let monitors: [PanelTab] = [.overview, .system, .cpu, .gpu, .memory, .disk, .network, .thermal]
static let tools: [PanelTab] = [.processes, .keepAwake, .cleaner]
static let settings: [PanelTab] = [.settingsGeneral, .settingsMenuBar, .settingsNotifications, .settingsHelper, .settingsAbout]
@@ -241,6 +241,7 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case .cpu: "CPU"
case .gpu: "GPU"
case .memory: "内存"
case .disk: "磁盘"
case .network: "网络"
case .thermal: "温度与风扇"
case .processes: "进程"
@@ -261,6 +262,7 @@ public enum PanelTab: String, CaseIterable, Identifiable, Sendable {
case .cpu: "cpu"
case .gpu: "square.3.layers.3d"
case .memory: "memorychip"
case .disk: "internaldrive"
case .network: "network"
case .thermal: "fan"
case .processes: "list.bullet.rectangle"
@@ -38,12 +38,22 @@ public final class MetricsStore {
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) }
@@ -44,3 +44,30 @@ import Testing
#expect(PowerSampler.joulesPerUnit("mJ") == 1e-3)
}
}
@Suite struct DiskSamplerTests {
@Test func parsesNVMeHealthLog() throws {
var log = [UInt8](repeating: 0, count: 512)
log[0] = 0
log[1] = 0x3B; log[2] = 0x01 // 315 K ≈ 41.85 °C
log[3] = 100; log[4] = 10; log[5] = 3
log[48] = 0x10; log[49] = 0x27 // 写入 10000 个数据单位
log[128] = 0xE8; log[129] = 0x03 // 通电 1000 小时
log[144] = 5
let health = try #require(DiskHealthReader.parse(log, model: "SSD"))
#expect(health.percentageUsed == 3)
#expect(health.remainingLife == 97)
#expect(health.bytesWritten == 10_000 * 512 * 1000)
#expect(health.powerOnHours == 1000)
#expect(health.unsafeShutdowns == 5)
#expect(abs((health.temperature ?? 0) - 41.85) < 0.01)
#expect(DiskHealthReader.parse([1, 2, 3], model: "SSD") == nil)
}
@Test func activityNeedsTwoSamples() {
var sampler = DiskActivitySampler()
#expect(sampler.sample(now: Date()) == nil)
let second = sampler.sample(now: Date().addingTimeInterval(1))
#expect(second.map { $0.readRate >= 0 && $0.writeRate >= 0 } ?? true)
}
}