进程 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

+4
View File
@@ -11,6 +11,10 @@
- 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。
#### 调整
- CPU 弹窗与主窗口 CPU 页的“按应用汇总”、仪表盘的“高占用进程”,CPU 改为占整机的比例:全部核心跑满为 100%,和顶部的总占用是同一把尺子。以前按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用动不动显示 99%、75%,容易误以为很吃 CPU;鼠标悬停在数值上仍可看到单核口径的数值。进程页保持单核口径,与活动监视器一致。
#### 样式
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
@@ -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")
+6 -2
View File
@@ -59,15 +59,19 @@ system language by default; switch it in Settings → General → Language.
<!-- changelog:start -->
<!-- Generated from CHANGELOG.md by Scripts/sync_changelog.py. Do not edit by hand. -->
Latest release **0.3.2** (2026-09-16) · **7** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
Latest release **0.3.2** (2026-09-16) · **8** changes in development · [full changelog](CHANGELOG.md) (kept in Chinese)
<details open>
<summary><b>2026-09-16</b> · Unreleased · 1 fixed · 5 style · 1 added</summary>
<summary><b>2026-09-16</b> · Unreleased · 1 fixed · 1 changed · 5 style · 1 added</summary>
**Fixed**
- 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。
**Changed**
- CPU 弹窗与主窗口 CPU 页的“按应用汇总”、仪表盘的“高占用进程”,CPU 改为占整机的比例:全部核心跑满为 100%,和顶部的总占用是同一把尺子。以前按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用动不动显示 99%、75%,容易误以为很吃 CPU;鼠标悬停在数值上仍可看到单核口径的数值。进程页保持单核口径,与活动监视器一致。
**Style**
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
+6 -2
View File
@@ -55,15 +55,19 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。
<!-- changelog:start -->
<!-- 由 Scripts/sync_changelog.py 从 CHANGELOG.md 生成,请勿手改。 -->
最新版本 **0.3.2**(2026-09-16) · 开发中 **7** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
最新版本 **0.3.2**(2026-09-16) · 开发中 **8** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
<details open>
<summary><b>2026-09-16</b> · 未发布 · 修复 1 · 样式 5 · 新增 1</summary>
<summary><b>2026-09-16</b> · 未发布 · 修复 1 · 调整 1 · 样式 5 · 新增 1</summary>
**修复**
- 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。
**调整**
- CPU 弹窗与主窗口 CPU 页的“按应用汇总”、仪表盘的“高占用进程”,CPU 改为占整机的比例:全部核心跑满为 100%,和顶部的总占用是同一把尺子。以前按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用动不动显示 99%、75%,容易误以为很吃 CPU;鼠标悬停在数值上仍可看到单核口径的数值。进程页保持单核口径,与活动监视器一致。
**样式**
- 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。
+1 -1
View File
@@ -13,7 +13,7 @@ settings:
SWIFT_VERSION: "6.0"
MARKETING_VERSION: "0.3.2"
# 构建号四位数字,每次 make build 加一;营销版本只在发布时用 Scripts/version.sh patch / minor 修改
CURRENT_PROJECT_VERSION: "0081"
CURRENT_PROJECT_VERSION: "0082"
MACOSX_DEPLOYMENT_TARGET: "14.0"
# 本地开发使用 ad-hoc 签名;发布时改为 Developer ID(见 docs/PLAN.md)
CODE_SIGN_STYLE: Manual