菜单栏做出来了,内存预算却没过:Hostwatch 开发手记(二)
项目地址:corn12138/hostwatch · MIT License
上一篇把 Hostwatch 的命令行原型搭了起来。能读取内存和磁盘,也能给出几条建议。但每次想知道电脑的情况,还得打开终端跑命令,离"随手看一眼"有点远。
接下来,我们把它放进了 Mac 菜单栏。
这一步确实做出了一个原生文字菜单:能看资源状态、查告警、调整采样间隔,也有暂停和按需读取进程排行的入口。底下仍然复用 Rust 的采集和判断逻辑,外面接 AppKit,没有加 WebView。
不过,这篇没办法以"低内存菜单栏工具完成了"收尾。之前定的暂定目标是 RSS 不超过 40 MiB,实际测下来,菜单还没打开就已经超过了。
先交代范围:本文记录的是 2026 年 10 月 19 日的本地开发阶段。菜单栏相关代码仍有未提交、未推送的改动,公开仓库暂不保证包含本文全部功能;后面提到的新浮层也还只是设计与开发任务。这里回顾的是已有代码和验证记录,没有把下一版的效果提前算进来。
从一个数字,变成两个样本之间发生的事
接菜单栏之前,采集先往前走了一步。
最早只有内存、swap 存量和磁盘空间。后来补了原生内存分项、CPU,以及交换换入、换出的速率。sysinfo 继续用于可选的进程排行,主要指标改由 macOS 原生接口读取,并分别记录采集结果。
速率的公式不难:
text
交换速率 = 两次样本的交换页数差 × 页大小 / 实际经过的秒数
麻烦的是,有些时候根本不该计算。
刚启动,只有一个样本;某个数据源失败,前后两次不完整;计数器重置,累计值反而变小;电脑睡了一觉,时间跨度也需要重新判断。
这些情况下填一个零,界面会很整齐,含义却错了。零表示测过、没有活动;缺失表示这次还不能判断。
所以数据里留下了预热、不可用、计数器重置和睡眠或时钟跳变等状态。固定样本能覆盖这些分支,但真正合盖、唤醒后的表现,还需要另外验收。
界面因此多了一些状态要处理。这个复杂度省不下来:数据层不区分,最后就只能让用户替我们猜。
看到一次异常,要不要马上提醒
持续采样以后,每次都打印同一句建议,就开始显得吵。
我们加了一层告警状态:普通警告需要两个有效异常样本,严重情况一个样本就触发;恢复需要两个有效正常样本。同一规则恢复后短时间又反复出现,会受十分钟冷却限制,更严重的升级仍然立即记录。
这里说的是程序里的告警事件,还没有接 macOS 系统通知。
有个细节值得单独说:未知样本既不证明异常,也不证明恢复。目前的实现会保留已有计数,不因一次未知而清空。
比如下面这段序列:
text
第一次:异常
第二次:数据不可用
第三次:异常
按当前规则,第三次已经有两个有效异常样本,可以触发警告。但这不能被解释成"已确认电脑连续异常了整整三个采样周期"。中间那段,我们没有证据。
同样,历史最多保存 120 个样本,也不代表固定的一个小时。采样间隔能改,记录中还可能有空缺,实际跨度必须看时间戳。
这些区别在终端里可以写一行说明,到了菜单栏和图表里,就得体现在真正的状态和文案里。
一个对勾,差点把缺失的数据藏起来
菜单栏空间很小,我们希望用短标题告诉用户现在是否需要关注。
复核时发现一个问题:CPU 详情可能写着"不可用",状态栏却显示 HW ✓。
原因并不复杂。程序检查了这一轮数据源是否成功,也检查了整体速率状态,但"这次读到了 CPU 计数"不等于"这次已经能算 CPU 利用率"。
假设上一个样本没有 CPU ticks,这次恢复读取,就仍然缺一份基线。还有一种情况是两次 ticks 没有差值,也算不出有效比例。这时其他速率已经就绪,CPU 却还是空的。
回归测试用实际的差分逻辑构造了这两种情况。修复前,它确实会给出正常标题;修复后,显示"部分数据不可用:CPU 占用"。下一次能算出数据,再恢复对应状态。已有持续告警也不会被这个提示盖掉。
这件事让我更在意总览文案了。详情里诚实地写了"不可用",并不能抵消首页一个绿色对勾的误导。很多人只会看那个对勾。
菜单打开的时候,定时器怎么办
另一个问题出在 AppKit 的 run loop。
原来的定时器只注册在默认模式。为了确认菜单跟踪模式下的行为,阶段验证里写了一个独立的小程序:两个定时器都设为 0.2 秒,主线程以菜单跟踪使用的模式运行 2.1 秒。
记录是这样的:
| 注册方式 | 触发次数 |
|---|---|
| 只在默认模式 | 0 |
| 加入 common modes | 10 |
产品里的采样定时器随后改为加入 NSRunLoopCommonModes。标题和摘要没变化时,也跳过重复设置,减少无意义的重绘。
这里还做了一个界面上的取舍:采样继续,已经展开的文字菜单保持打开时的快照,重新打开才刷新内容。这样不会在用户正准备点某一项时重建整份菜单。状态栏标题、告警和历史则继续随采样更新。
这份小程序验证了定时器机制,但它不能代替真实菜单的点击测试。那一轮桌面验收受辅助功能和屏幕录制权限限制,菜单展开期间的完整行为没有通过真实操作确认,记录里保留了这个缺口。
然后,内存预算没过
最早给菜单栏版暂定的是 40 MiB RSS。
命令行阶段的资源记录看着还不错:不开进程排行,5 秒采一次、观察 175 秒,峰值 RSS 约 5.6 MiB。可接上 AppKit 之后,不能继续引用这个数字了。
菜单栏阶段留下了一次 30 分钟测量。环境是 Apple Silicon、48 GiB 内存、macOS 27.2,release 构建,采样间隔 5 秒。记录同时读取 RSS、物理内存足迹和 CPU 时间。
| 这次观测 | RSS | 物理内存足迹 |
|---|---|---|
| 前 150 秒,菜单未打开 | 约 65.5--67.4 MiB | 约 12.55--12.67 MB |
| 中途出现一次阶跃后 | 约 96 MiB | 约 25.8 MB |
| 整个 30 分钟的最大值 | 96.6 MiB | 26.46 MB |
两列单位沿用记录:MiB 按 2 的幂换算,MB 按十进制换算。它们本身也是不同指标,不能把数字拿来直接互换。
全程累计 CPU 时间约 2.14 秒,折算平均约为单核的 0.119%。这只能说明这次场景的平均 CPU 开销较小。
中途那次内存跳升,也不能随便解释。测量期间 GUI 没有受控,记录里只能确认大约 180--210 秒发生了阶跃,不能确定就是某次打开菜单造成的。之后约 26 分钟没有持续增长,也不等于证明长期无泄漏。
所以最直接的结论还是:原来那条 40 MiB RSS 目标,没有达到。
可以讨论是否更适合用物理内存足迹作为主要预算,同时保留 RSS 对照。但这是预算口径的调整,需要单独决定,不能看到另一列数字更小,就悄悄把这轮验收改成通过。
现有记录也解释不了所有 AppKit 应用的成本。一个版本、一台机器,推不出"原生菜单栏最低就得占这么多"的普遍结论。
这次比较有用的结果,是把账记得更具体了。接下来优化或换界面,可以在同样的场景下复测,不必凭印象争论哪个数字才算数。
文字菜单之后,还要重新想怎么展示
文字菜单能承载功能,但把内存、CPU、磁盘、采集说明、建议全塞进去,仍然容易变成另一张技术清单。
后续的浮层设计把首页收得更简单:一句状态、三条资源信息,有问题时再给一条优先建议。需要细看,再进入资源详情或处理方法。
不过,设计评审也发现了一些不能原样搬进代码的地方。比如首页 CPU 是一个数,图表末点却是另一个数;历史跨度按当前采样间隔重新推算;恢复状态看起来像要点了"打开活动监视器"才开始观察。
这些在演示里容易忽略,接上真实数据就会出问题。告警引擎一直在观察,点击系统工具既不启动恢复,也不能证明问题解决。图表和文字也必须来自同一份数据。
目前这部分仍是设计原型和原生实现任务。原型里的图表、模拟启动成功、退出动画,都不能作为已经完成的原生功能来展示。现有文字菜单的内存结果,同样不能拿来替下一版浮层验收。
做到这一步,Hostwatch 已经不只是把几个数打印出来了。它开始需要对"正常""未知""恢复"这些很短的词负责。
后面继续做界面时,我想保留这次踩过的界限:能少展示一点,但不能把不知道的事说成知道了。至于内存预算,等新界面真正跑起来,再把同一套测量拿出来算一次。
本文对照的项目材料:
- 阶段验证记录:资源测量条件、固定样本测试及未验收项。
- 30 分钟资源原始记录。
- run loop 小程序的输出。
- 浮层设计评审:设计通过与原生实现之间的边界。
上一篇:电脑卡了,然后呢?我想用 Rust 做一个 Mac 诊断小工具。
记录日期:2026-10-19。本文引用既有阶段测试与测量记录,没有因撰写文章重新执行整套测试。发布到掘金时,文末相对链接需随对应材料同步到公开仓库后转换为可访问链接。