进程 CPU 在概览里改为占整机的比例,悬停显示单核口径

用户反馈 CPU 占用高:CPU 弹窗“按应用汇总”与仪表盘“高占用进程”按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用常显示 99%、75%,又放在整机总占用下面,容易误读。

- Format.machineShare:单核口径 ÷ 逻辑核心数,全部核心跑满为 100%;小于 0.1% 且不为 0 写 “<0.1%”。Format.coreShare 保留单核口径写法
- CPU 弹窗 / CPU 页的按应用汇总、仪表盘高占用进程改用整机占比,数值悬停显示单核口径;仪表盘着色阈值改为整机占比 25% / 50%
- 进程页保持单核口径,与活动监视器一致
- 单元测试覆盖两种写法;译文与更新日志同步
This commit is contained in:
gentpan committed 2026-09-16 20:53:52 +05:00
1 parent 165963de84
commit 8f7691d290
9 files changed
+58 -14

No files matched your search

@@ -274,6 +274,7 @@ npm 缓存 npm Cache
协议版本 Protocol version
单行 Inline
单行文字 Inline Text
占整机 CPU 的比例:{} 个核心全部跑满为 100% Share of total CPU: all {} cores fully busy is 100%
占用 Usage
占用超过 85% 时数值与图形显示为红色 Show values and graphs in red above 85%
占用超过 85%(电池电量低于 20%)时数值与图形显示为红色 Values and graphics turn red above 85% usage (below 20% battery)
@@ -537,6 +538,7 @@ npm 缓存 npm Cache
拷贝 PID {} Copy PID {}
持续时间 Duration
按下快捷键… Press shortcut…
按单核满载为 100% 计:{} Per core (one full core = 100%): {}
按应用 By App
按应用汇总 By App
按文件夹 By folder
@@ -700,7 +702,7 @@ npm 缓存 npm Cache
正常 Normal
此刻各核心的占用;超过 60% 变橙色、85% 变红色 Current load of each core; orange above 60%, red above 85%
此设备没有可调节的风扇 This Mac has no adjustable fans
每个应用按一个核心满载为 100% 计算,与活动监视器一致,所以各应用相加可以超过顶部的总占用 Each app is measured with one full core as 100%, like Activity Monitor, so the apps can add up to more than the total above
每个应用占整机 CPU 的比例:{} 个核心全部跑满为 100%,与顶部的总占用是同一把尺子。鼠标悬停在数值上可以看按单核计的占用(活动监视器的算法) Each app's share of total CPU: all {} cores fully busy is 100%, the same scale as the total above. Hover a value to see it per core (the Activity Monitor way)
每个指标一个图标,点击弹出该项详情 One icon per metric; click for its details
每分钟把主要指标的平均值与峰值写入本机数据库,保留 7 天,不上传 Writes averages and peaks of key metrics to a local database every minute. Kept for 7 days, never uploaded.
每核 {} {} per core
@@ -24,6 +24,22 @@ public enum Format {
private static let units = ["B", "KB", "MB", "GB", "TB", "PB"]
/// 逻辑核心数(性能核与能效核合计),进程 CPU 换算成整机占比时用
public static let logicalCores = max(1, ProcessInfo.processInfo.activeProcessorCount)
/// 进程 CPU 按单核满载为 100% 的写法(活动监视器、top 的算法),多线程进程可以超过 100%。例:99.5%
public static func coreShare(_ value: Double) -> String {
"\((max(0, value) * 100).formatted(.number.precision(.fractionLength(1))))%"
}
/// 进程 CPU 占整机的比例:全部核心跑满为 100%,和系统总占用是同一把尺子。`value` 是单核口径。
/// 小于 0.1% 但不为 0 时写 “<0.1%”,不把正在跑的进程显示成 0
public static func machineShare(_ value: Double) -> String {
let share = max(0, value) / Double(logicalCores) * 100
if share > 0, share < 0.05 { return "<0.1%" }
return "\(share.formatted(.number.precision(.fractionLength(1))))%"
}
public static func bytes(_ value: UInt64, base: ByteBase = .binary) -> String {
bytes(Double(value), base: base)
}
@@ -425,6 +425,7 @@ private struct TopProcessesCard: View {
Text(tr("高占用进程")).dsFont(.xs, weight: .semibold)
Spacer()
Text("CPU").dsFont(.xs, weight: .medium).frame(width: DS.Size.valueColumn, alignment: .trailing)
.help(tr("占整机 CPU 的比例:\(Format.logicalCores) 个核心全部跑满为 100%"))
Text(tr("内存")).dsFont(.xs, weight: .medium).frame(width: DS.Size.valueColumn, alignment: .trailing)
Color.clear.frame(width: DS.Size.iconInline)
}
@@ -445,9 +446,10 @@ private struct TopProcessesCard: View {
.foregroundStyle(DS.Palette.textPrimary)
.lineLimit(1)
Spacer(minLength: DS.Space.s2)
Text(verbatim: cpuText(process.cpu))
Text(verbatim: Format.machineShare(process.cpu))
.dsFont(.sm, weight: .medium)
.foregroundStyle(cpuTone(process.cpu).color)
.help(tr("按单核满载为 100% 计:\(Format.coreShare(process.cpu))"))
.frame(width: DS.Size.valueColumn, alignment: .trailing)
Text(verbatim: Format.bytes(process.memory))
.dsFont(.sm)
@@ -493,12 +495,10 @@ private struct TopProcessesCard: View {
}
}
private func cpuText(_ value: Double) -> String {
"\((value * 100).formatted(.number.precision(.fractionLength(1))))%"
}
/// 按占整机的比例着色:一个进程吃掉整机一半算很高,四分之一算偏高(`value` 是单核口径)
private func cpuTone(_ value: Double) -> Tone {
value >= 0.8 ? .error : value >= 0.5 ? .warning : .neutral
let share = value / Double(Format.logicalCores)
return share >= 0.5 ? .error : share >= 0.25 ? .warning : .neutral
}
}
@@ -97,7 +97,7 @@ struct CPUPopover: View {
}
case .cpuApps:
SectionCard(title: section.title,
hint: tr("每个应用按一个核心满载为 100% 计算,与活动监视器一致,所以各应用相加可以超过顶部的总占用")) {
hint: tr("每个应用占整机 CPU 的比例:\(Format.logicalCores) 个核心全部跑满为 100%,与顶部的总占用是同一把尺子。鼠标悬停在数值上可以看按单核计的占用(活动监视器的算法)")) {
AppUsageList(apps: AppUsage.group(store.processes).sorted { $0.cpu > $1.cpu },
rowCount: isDetailPage ? 10 : 6, metric: .cpu)
}
@@ -536,6 +536,7 @@ private struct AppUsageList: View {
.dsFont(.xs, weight: .medium)
.monospacedDigit()
.foregroundStyle(DS.Palette.textSecondary)
.help(optional: metric == .cpu ? tr("按单核满载为 100% 计:\(Format.coreShare(app.cpu))") : nil)
}
ProgressTrack(fraction: peak > 0 ? value / peak : 0, color: DS.Palette.primary, height: DS.Space.s1, showsTrack: false)
.padding(.leading, DS.Size.iconInline + DS.Space.s2)
@@ -547,7 +548,8 @@ private struct AppUsageList: View {
private func valueText(_ app: AppUsage) -> String {
switch metric {
case .cpu:
return "\((app.cpu * 100).formatted(.number.precision(.fractionLength(1))))%"
// 占整机的比例;单核口径的数值放在悬停提示里
return Format.machineShare(app.cpu)
case .memory:
guard let total, total > 0 else { return Format.bytes(app.memory) }
return "\(Format.bytes(app.memory)) · \(Format.percent(Double(app.memory) / Double(total)))"
@@ -52,6 +52,18 @@ struct FormatTests {
#expect(Format.bytes(UInt64(412_000_000_000), base: .decimal) == "412 GB")
}
@Test func formatsProcessCPU() {
// 单核口径:一个核跑满是 100%,多线程可以超过
#expect(Format.coreShare(0.995) == "99.5%")
#expect(Format.coreShare(2.5) == "250.0%")
// 整机口径:全部核心跑满是 100%
let cores = Double(Format.logicalCores)
#expect(Format.machineShare(cores) == "100.0%")
#expect(Format.machineShare(cores / 2) == "50.0%")
#expect(Format.machineShare(0) == "0.0%")
#expect(Format.machineShare(0.0001) == "<0.1%")
}
@Test func menuBarRate() {
#expect(Format.menuBarRate(0) == "0 B/s")
#expect(Format.menuBarRate(512) == "512 B/s")