perf(cpu): 减少菜单栏读取和磁盘排行重复计算

This commit is contained in:
ysicing authored and 缘生 committed 2026-10-01 11:18:40 +08:00
1 parent a068fb3e38
commit 87079eddb2
6 files changed
+184 -44

No files matched your search

@@ -77,7 +77,7 @@ final class MenuBarController: NSObject {
func refreshImages(force: Bool = false) {
let settings = model.settings
let reading = MenuBarReading(model: model)
let reading = MenuBarReading(model: model, items: model.visibleMenuBarItems)
for (index, entry) in items.enumerated() {
let itemsToDraw = entry.key.map { [$0] } ?? model.visibleMenuBarItems
var entryReading = reading
@@ -58,45 +58,60 @@ struct MenuBarReading {
/// 蓝牙设备电量低于这个百分比才在菜单栏提示
static let lowBluetoothPercent = 20
/// 只读取展示项目所需的指标,避免单项预览观察无关采样并重复栅格化。
/// 默认仍提供全部读数,供完整读数调用方使用;防休眠标记与项目无关,始终保留。
@MainActor
init(model: AppModel) {
init(model: AppModel, items: [MenuBarItem] = MenuBarItem.allCases) {
let store = model.store
cpu = store.cpu?.total
cpuHistory = store.cpuTotal.elements
gpu = store.gpu?.utilization
gpuHistory = store.gpuHistory.elements
memory = store.memory?.usedFraction
memoryHistory = store.memoryHistory.elements
disk = store.disk?.usedFraction
diskHistory = disk.map { Array(repeating: $0, count: 30) } ?? []
upload = store.network?.uploadBytesPerSecond
download = store.network?.downloadBytesPerSecond
networkLocationStyle = model.settings.publicIPLookup ? model.settings.networkLocationStyle : .off
networkCountryCode = model.network.publicAddresses?.countryCode
temperature = store.sensors?.temperature(.cpu)?.maximum
fanRPM = store.fastestFan?.current
battery = store.battery?.level
batteryHistory = battery.map { Array(repeating: $0, count: 30) } ?? []
aiTokens = model.aiUsage.todayTokens
aiUsageShowsLocalUsage = model.settings.aiUsageShowsLocalUsage
aiQuotas = model.aiUsage.visibleQuotaProviders(for: nil).compactMap { provider in
let state = model.aiUsage.visibleQuotaState(for: provider)
guard let snapshot = state.snapshot else { return nil }
// 已过重置时间的窗口不再代表当前额度(常见于重启后离线恢复的缓存),不显示过期额度。
let now = Date()
let windows = snapshot.windows.filter { $0.resetsAt.map { $0 > now } ?? true }
// 周额度比短时会话额度更适合作为常驻读数;模型专属周额度仅在通用周额度缺席时使用。
let window = windows.first(where: { $0.kind == .weekly })
?? windows.first(where: { $0.kind == .fableWeekly })
?? windows.first(where: { $0.kind == .opusWeekly || $0.kind == .sonnetWeekly })
?? windows.first(where: { $0.kind == .session })
return window.map { MenuBarQuota(provider: provider, window: $0, source: snapshot.source,
fetchedAt: snapshot.fetchedAt, isStale: state.isStale) }
}
batteryCharging = store.battery?.isCharging ?? false
if let lowest = model.bluetooth.lowest,
battery == nil || (model.settings.bluetoothLowBatteryInMenuBar && lowest.percent <= Self.lowBluetoothPercent) {
bluetoothDevice = (lowest.device.kind.symbol, lowest.percent)
for item in items {
switch item {
case .cpu:
cpu = store.cpu?.total
cpuHistory = store.cpuTotal.elements
case .gpu:
gpu = store.gpu?.utilization
gpuHistory = store.gpuHistory.elements
case .memory:
memory = store.memory?.usedFraction
memoryHistory = store.memoryHistory.elements
case .disk:
disk = store.disk?.usedFraction
diskHistory = disk.map { Array(repeating: $0, count: 30) } ?? []
case .network:
upload = store.network?.uploadBytesPerSecond
download = store.network?.downloadBytesPerSecond
networkLocationStyle = model.settings.publicIPLookup ? model.settings.networkLocationStyle : .off
networkCountryCode = model.network.publicAddresses?.countryCode
case .temperature:
temperature = store.sensors?.temperature(.cpu)?.maximum
case .fan:
fanRPM = store.fastestFan?.current
case .battery:
battery = store.battery?.level
batteryHistory = battery.map { Array(repeating: $0, count: 30) } ?? []
batteryCharging = store.battery?.isCharging ?? false
if let lowest = model.bluetooth.lowest,
battery == nil || (model.settings.bluetoothLowBatteryInMenuBar && lowest.percent <= Self.lowBluetoothPercent) {
bluetoothDevice = (lowest.device.kind.symbol, lowest.percent)
}
case .aiUsage:
aiTokens = model.aiUsage.todayTokens
aiUsageShowsLocalUsage = model.settings.aiUsageShowsLocalUsage
aiQuotas = model.aiUsage.visibleQuotaProviders(for: nil).compactMap { provider in
let state = model.aiUsage.visibleQuotaState(for: provider)
guard let snapshot = state.snapshot else { return nil }
// 已过重置时间的窗口不再代表当前额度(常见于重启后离线恢复的缓存),不显示过期额度。
let now = Date()
let windows = snapshot.windows.filter { $0.resetsAt.map { $0 > now } ?? true }
// 周额度比短时会话额度更适合作为常驻读数;模型专属周额度仅在通用周额度缺席时使用。
let window = windows.first(where: { $0.kind == .weekly })
?? windows.first(where: { $0.kind == .fableWeekly })
?? windows.first(where: { $0.kind == .opusWeekly || $0.kind == .sonnetWeekly })
?? windows.first(where: { $0.kind == .session })
return window.map { MenuBarQuota(provider: provider, window: $0, source: snapshot.source,
fetchedAt: snapshot.fetchedAt, isStale: state.isStale) }
}
}
}
keepAwake = model.keepAwake.isActive
}
@@ -258,8 +273,9 @@ enum MenuBarRenderer {
static func image(for model: AppModel) -> NSImage {
let settings = model.settings
return image(reading: MenuBarReading(model: model),
items: model.visibleMenuBarItems,
let items = model.visibleMenuBarItems
return image(reading: MenuBarReading(model: model, items: items),
items: items,
style: { settings.style(for: $0) },
networkStyle: settings.networkStyle,
colorizeHighLoad: settings.colorizeHighLoad,
@@ -489,7 +489,7 @@ private struct ItemStyleRow: View {
/// 这一项按当前风格、实时读数画出来的样子,底色跟随浅色 / 深色外观
private var preview: some View {
let settings = model.settings
let image = MenuBarRenderer.image(reading: MenuBarReading(model: model), items: [item],
let image = MenuBarRenderer.image(reading: MenuBarReading(model: model, items: [item]), items: [item],
style: { settings.style(for: $0) }, networkStyle: settings.networkStyle,
colorizeHighLoad: settings.colorizeHighLoad, fahrenheit: settings.useFahrenheit)
return Image(nsImage: MenuBarRenderer.preview(image, dark: colorScheme == .dark))
@@ -108,10 +108,18 @@ public final class MetricsStore {
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
// 排行只需要磁盘计数,不必构建带名称、文案和其他指标的完整界面行。
// 分组键与 ProcessRowModel 保持一致;同组有读取计数时才采用写入合计,
// 保留混合权限进程和缺失计数的既有语义,并分别累加读写以保持浮点运算顺序。
var totals: [String: (read: Double?, write: Double)] = [:]
for process in processes {
let id = process.appBundlePath.map { "app-\($0)" } ?? "pid-\(process.pid)"
var total = totals[id] ?? (nil, 0)
if let read = process.diskRead { total.read = (total.read ?? 0) + read }
total.write += process.diskWrite ?? 0
totals[id] = total
}
let activity = totals.mapValues { total in total.read.map { $0 + total.write } ?? 0 }
diskRanking.update(activity)
}
if let counts = snapshot.systemCounts { systemCounts = counts }
@@ -0,0 +1,81 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import AppKit
import Foundation
import Observation
import SMC
import Testing
@testable import Metrics
@testable import XStatsUI
@MainActor
struct MenuBarReadingTests {
@Test func batteryPreviewIgnoresCPUAndStillObservesBattery() async throws {
let suite = "MenuBarReadingTests.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let model = AppModel(settings: AppSettings(defaults: defaults), historyURL: nil,
aiUsageProviders: [], aiQuotaProviders: [])
await confirmation(expectedCount: 0) { changed in
withObservationTracking {
_ = MenuBarReading(model: model, items: [.battery])
} onChange: { changed() }
var snapshot = MetricsSnapshot()
snapshot.cpu = CPULoad(total: 0.5, user: 0.3, system: 0.2, perCore: [0.5], loadAverage: [1, 1, 1])
model.store.apply(snapshot)
}
await confirmation(expectedCount: 1) { changed in
withObservationTracking {
_ = MenuBarReading(model: model, items: [.battery])
} onChange: { changed() }
var snapshot = MetricsSnapshot()
snapshot.battery = BatteryStatus(level: 0.75, isCharging: true, isPluggedIn: true,
isFullyCharged: false, minutesRemaining: 20, cycleCount: 30,
health: 0.98, adapterWatts: 60)
model.store.apply(snapshot)
}
}
@Test func scopedReadingsPreserveEveryItemImage() throws {
let suite = "MenuBarReadingTests.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let model = AppModel(settings: AppSettings(defaults: defaults), historyURL: nil,
aiUsageProviders: [], aiQuotaProviders: [])
var snapshot = MetricsSnapshot()
snapshot.cpu = CPULoad(total: 0.83, user: 0.6, system: 0.23, perCore: [0.83], loadAverage: [1, 1, 1])
snapshot.gpu = GPUUsage(name: "Fixture", utilization: 0.3, coreCount: 8)
snapshot.memory = MemoryUsage(total: 1_000, app: 300, wired: 100, compressed: 50, cached: 200,
swapUsed: 0, swapTotal: 0, pressure: .normal, uncompressed: 100,
swapInBytes: 0, swapOutBytes: 0)
snapshot.sensors = SensorReadings(temperatures: [TemperatureSummary(group: .cpu, average: 50,
maximum: 60, sensorCount: 4)],
fans: [FanState(id: 0, current: 1_800, minimum: 0, maximum: 6_000,
target: 1_800, isManual: false)], fanCount: 1)
snapshot.disk = DiskUsage(volumeName: "Fixture", total: 1_000, available: 350)
snapshot.network = NetworkRate(downloadBytesPerSecond: 1_500_000, uploadBytesPerSecond: 20_000,
totalDownloaded: 10_000_000, totalUploaded: 100_000)
snapshot.battery = BatteryStatus(level: 0.15, isCharging: true, isPluggedIn: true,
isFullyCharged: false, minutesRemaining: 20, cycleCount: 30,
health: 0.98, adapterWatts: 60)
model.store.apply(snapshot)
for item in MenuBarItem.allCases {
let complete = MenuBarReading(model: model)
let scoped = MenuBarReading(model: model, items: [item])
#expect(scoped.tooltip(items: [item], fahrenheit: false) == complete.tooltip(items: [item], fahrenheit: false))
for style in MenuBarStyle.options(for: item) {
#expect(scoped.hasSameImage(as: complete, item: item, style: style))
let completeImage = MenuBarRenderer.image(reading: complete, items: [item], style: { _ in style },
networkStyle: .dots, colorizeHighLoad: true, fahrenheit: false)
let scopedImage = MenuBarRenderer.image(reading: scoped, items: [item], style: { _ in style },
networkStyle: .dots, colorizeHighLoad: true, fahrenheit: false)
for dark in [false, true] {
let before = try #require(MenuBarRenderer.preview(completeImage, dark: dark).tiffRepresentation)
let after = try #require(MenuBarRenderer.preview(scopedImage, dark: dark).tiffRepresentation)
#expect(before == after)
}
}
}
}
}
@@ -0,0 +1,35 @@
// Copyright (C) 2026 ysicing
// SPDX-License-Identifier: AGPL-3.0-or-later
import Foundation
import Testing
import Metrics
@testable import XStatsUI
@MainActor
struct MetricsStoreTests {
@Test func diskRankingPreservesGroupingAndDecay() {
let store = MetricsStore()
var expected = ActivityRanking()
let processes = (0..<1_000).map { index in
var process = ProcessUsage(pid: Int32(index + 1), name: "worker-\(index)", executablePath: nil,
appBundlePath: index % 3 == 0 ? nil : "/Applications/Fixture\(index % 17).app",
cpu: Double(index % 10) / 100, memory: UInt64(index * 1_024))
process.diskRead = index % 5 == 0 ? nil : Double(index * 17)
process.diskWrite = index % 7 == 0 ? nil : Double(index * 11)
return process
}
// 包括单独写计数、同应用混合权限、进程退出,以及空采样后的平滑衰减。
for batch in [processes, Array(processes.prefix(200)), [], [], []] {
let activity = Dictionary(uniqueKeysWithValues: ProcessRowModel.grouped(batch).map {
($0.id, $0.disk.map { $0.read + $0.write } ?? 0)
})
expected.update(activity)
var snapshot = MetricsSnapshot()
snapshot.processes = batch
store.apply(snapshot)
#expect(store.diskRanking == expected)
#expect(store.processes == batch)
}
}
}