进程 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:
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")
|
||||
|
||||
Reference in new issue
Block a user