From 8f7691d290083e915948674f7a979617815affaa Mon Sep 17 00:00:00 2001 From: gentpan Date: Wed, 16 Sep 2026 20:53:52 +0500 Subject: [PATCH] =?UTF-8?q?=E8=BF=9B=E7=A8=8B=20CPU=20=E5=9C=A8=E6=A6=82?= =?UTF-8?q?=E8=A7=88=E9=87=8C=E6=94=B9=E4=B8=BA=E5=8D=A0=E6=95=B4=E6=9C=BA?= =?UTF-8?q?=E7=9A=84=E6=AF=94=E4=BE=8B=EF=BC=8C=E6=82=AC=E5=81=9C=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=8D=95=E6=A0=B8=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈 CPU 占用高:CPU 弹窗“按应用汇总”与仪表盘“高占用进程”按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用常显示 99%、75%,又放在整机总占用下面,容易误读。 - Format.machineShare:单核口径 ÷ 逻辑核心数,全部核心跑满为 100%;小于 0.1% 且不为 0 写 “<0.1%”。Format.coreShare 保留单核口径写法 - CPU 弹窗 / CPU 页的按应用汇总、仪表盘高占用进程改用整机占比,数值悬停显示单核口径;仪表盘着色阈值改为整机占比 25% / 50% - 进程页保持单核口径,与活动监视器一致 - 单元测试覆盖两种写法;译文与更新日志同步 --- CHANGELOG.md | 4 ++++ .../Sources/Localization/Translations.swift | 4 +++- .../Sources/Metrics/Formatting.swift | 16 ++++++++++++++++ .../Sources/OpenStatsUI/Panel/OverviewPage.swift | 12 ++++++------ .../OpenStatsUI/Popover/MetricPopovers.swift | 6 ++++-- .../Tests/MetricsTests/MetricsTests.swift | 12 ++++++++++++ README.md | 8 ++++++-- README.zh-CN.md | 8 ++++++-- project.yml | 2 +- 9 files changed, 58 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c22d493..58bd591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。 +#### 调整 + +- CPU 弹窗与主窗口 CPU 页的“按应用汇总”、仪表盘的“高占用进程”,CPU 改为占整机的比例:全部核心跑满为 100%,和顶部的总占用是同一把尺子。以前按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用动不动显示 99%、75%,容易误以为很吃 CPU;鼠标悬停在数值上仍可看到单核口径的数值。进程页保持单核口径,与活动监视器一致。 + #### 样式 - 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。 diff --git a/Packages/OpenStatsKit/Sources/Localization/Translations.swift b/Packages/OpenStatsKit/Sources/Localization/Translations.swift index fa30aa3..3ae57ad 100644 --- a/Packages/OpenStatsKit/Sources/Localization/Translations.swift +++ b/Packages/OpenStatsKit/Sources/Localization/Translations.swift @@ -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 diff --git a/Packages/OpenStatsKit/Sources/Metrics/Formatting.swift b/Packages/OpenStatsKit/Sources/Metrics/Formatting.swift index c63cfc2..58c54f6 100644 --- a/Packages/OpenStatsKit/Sources/Metrics/Formatting.swift +++ b/Packages/OpenStatsKit/Sources/Metrics/Formatting.swift @@ -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) } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/OverviewPage.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/OverviewPage.swift index 03b975a..5aaf924 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/OverviewPage.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Panel/OverviewPage.swift @@ -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 } } diff --git a/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift b/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift index f2b3ceb..ffc09ea 100644 --- a/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift +++ b/Packages/OpenStatsKit/Sources/OpenStatsUI/Popover/MetricPopovers.swift @@ -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)))" diff --git a/Packages/OpenStatsKit/Tests/MetricsTests/MetricsTests.swift b/Packages/OpenStatsKit/Tests/MetricsTests/MetricsTests.swift index 39dbf56..2e43f63 100644 --- a/Packages/OpenStatsKit/Tests/MetricsTests/MetricsTests.swift +++ b/Packages/OpenStatsKit/Tests/MetricsTests/MetricsTests.swift @@ -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") diff --git a/README.md b/README.md index 9c7144b..bafdc60 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,19 @@ system language by default; switch it in Settings → General → Language. -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)
-2026-09-16 · Unreleased · 1 fixed · 5 style · 1 added +2026-09-16 · Unreleased · 1 fixed · 1 changed · 5 style · 1 added **Fixed** - 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。 +**Changed** + +- CPU 弹窗与主窗口 CPU 页的“按应用汇总”、仪表盘的“高占用进程”,CPU 改为占整机的比例:全部核心跑满为 100%,和顶部的总占用是同一把尺子。以前按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用动不动显示 99%、75%,容易误以为很吃 CPU;鼠标悬停在数值上仍可看到单核口径的数值。进程页保持单核口径,与活动监视器一致。 + **Style** - 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。 diff --git a/README.zh-CN.md b/README.zh-CN.md index f585840..3af7b55 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -55,15 +55,19 @@ CPU 不分性能核与能效核,部分功耗与频率读数可能不显示。 -最新版本 **0.3.2**(2026-09-16) · 开发中 **7** 项改动尚未发布 · [完整更新日志](CHANGELOG.md) +最新版本 **0.3.2**(2026-09-16) · 开发中 **8** 项改动尚未发布 · [完整更新日志](CHANGELOG.md)
-2026-09-16 · 未发布 · 修复 1 · 样式 5 · 新增 1 +2026-09-16 · 未发布 · 修复 1 · 调整 1 · 样式 5 · 新增 1 **修复** - 网络弹窗偶尔差一点放不下、露出一小截滚动条:弹窗打开时按当时的内容算高度,之后公网 IP、归属地等数据陆续到达,内容变高了窗口却不跟着变。现在内容一变高或变矮,窗口高度随之调整到正好放下,只有屏幕放不下时才出现滚动条;展开、收起区块时同样适用。 +**调整** + +- CPU 弹窗与主窗口 CPU 页的“按应用汇总”、仪表盘的“高占用进程”,CPU 改为占整机的比例:全部核心跑满为 100%,和顶部的总占用是同一把尺子。以前按单核满载为 100% 计(活动监视器的算法),多核 Mac 上一个应用动不动显示 99%、75%,容易误以为很吃 CPU;鼠标悬停在数值上仍可看到单核口径的数值。进程页保持单核口径,与活动监视器一致。 + **样式** - 主窗口在 macOS 26 上改用液态玻璃:侧边栏是一块浮在窗口里的玻璃面板,红绿灯按钮落在面板里,选中项垫一块淡蓝色圆角底并随切换滑动;顶栏右侧“在菜单栏显示”一组收进玻璃胶囊。主按钮是蓝色玻璃胶囊、次要按钮与图标按钮是透明玻璃,风扇模式等快捷切换是玻璃胶囊,开关换成系统开关(拖动时滑块变成玻璃)。设置分组与数据卡片保持实色,与系统设置一致。macOS 14、15 上保持原来的样式。 diff --git a/project.yml b/project.yml index cbe73ae..5df0936 100644 --- a/project.yml +++ b/project.yml @@ -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