让应用仅仅保持打开时不占用 CPU 的方法
编辑器虽然一整天都开着,但真正打字的时间只占其中一部分。mote 在其余时间里既不调度帧,也不设置定时器,并通过代码检查和真机测量两道关卡来确保这一点。

编辑器是笔记本电脑上打开时间最长的应用。但真正用于写作的时间并不长。大多数时候,它只是在你做其他事情时安静地开着。
因此,mote 定下了一条原则。
如果文档没有变化,也没有动画,就什么也不做。
没有预定帧,没有重复定时器,也没有周期性检查。这不是优化,而是一条必须优先于所有功能遵守的设计原则。
如果无事发生,帧数也为 0
仅凭 CPU 使用率很难判断应用是否安静。因为每次测量时数值都会波动,而且经过四舍五入后很容易变成 0。
所以 mote 首先看帧数。窗口打开后,如果文档在 10 秒内没有变化,绘制的帧数就必须恰好为 0。帧数直接在应用内部计数,只要绘制了一帧,代码就不会被合并。
即使帧数为 0,也不能放心。因为线程可能会在看不见的地方不断被唤醒。我们也会同时记录进程中的所有线程每秒被唤醒多少次。由于目前还在积累基准,因此不会仅凭这项数值阻止开发。
唯一的例外是插入符闪烁。它不使用重复定时器,而是在闪烁一次后重新调度下一次动作。窗口一旦失去焦点,就会立即停止。包括插入符在内的空闲 CPU 预算是 0.3%。
我们不会只单独看空闲性能。推迟绘制虽然很容易让帧数变成 0,但输入的文字可能会延迟出现。因此,我们会同时检查按键输入延迟、启动速度和内存。只要有一项超过标准,代码就不会被合并。

在代码中检查一次,在真实设备上再测一次
原则会随着时间推移而变得模糊。所以我们会确认两次。
首先检查代码。自动查找重复定时器、永不结束的 tick、反复休眠和唤醒的轮询,以及不必要地重新读取整个文件的代码。一旦发现,CI 就会在那里停止。如果确实是必需的例外,就必须在代码正上方说明为什么该任务会结束。
接下来在真实设备上测量。这时要遵守四项条件。
在配有 GPU 的真实屏幕上测量。指针放在窗口外。使用真实的会话总线,并在帧被绘制的瞬间立即计数。
这些条件都是经历失败后形成的。曾经仅仅因为指针停在窗口上,就产生了 150 个空闲帧。在隔离的会话中,工具包会因为等待响应而使启动延迟数秒。使用延迟上报的帧计数器时,启动画面的帧还曾混入空闲区间。
内存也不按绝对值判断。因为工具包和图形驱动程序占用的内存会随环境变化数十 MB。
我们改用空白文档作为基准。只看打开文档后比空白文档多用了多少内存。最近一次测量中,空白文档使用了 116MB,100KB 文档使用了 120MB。mote 额外使用的内存是 4MB,处于 8MB 的预算之内。
一开始并不是这样测量的。我们曾以为超出了内存预算,后来才发现增加的并不是应用的内存,而是运行环境的基础内存。从那天起,我们开始测量的就不再是绝对值,而是“我们的功能所增加的部分”。

开发时,屏幕上还会显示仍在运行的定时器数量。如果不是 0,就能在提交前立即发现。
自动保存也遵循这项原则。只有文档发生变化时才设置一次定时器,保存完成后就将其移除。它不会周期性地唤醒并检查“有没有发生变化?”
这些数字不是为了战胜竞争对手
一开始,我们想把空闲性能作为优于 Typora 的特点来介绍。但亲自测量后,发现事实并非如此。
在相同设备和相同条件下,Typora 的空闲 CPU 使用率是 0.1%。实际上与 mote 相同。确认存在差异的应用是 Obsidian,为 0.8%;MarkText 为 0.6%。
我们也曾写错过。有一段时间,文档中写着“Typora 开着时会占用 CPU”。作为依据的 34.6% 并不是 Typora 的测量值,而是 apostrophe 的测量值。重新确认后,我们立即修改了这句话。
空闲帧数为 0 并不是针对竞争对手的主张。它是 mote 对自身施加的纪律。因为如果没有反复运行的任务,我们就能随时解释应用此刻正在做什么。
因为这项原则,我们也放弃了一些东西
很多时候,我们不能采用方便的方法。
不能通过周期性检查来判断文件是否在外部发生了变化,而必须接收操作系统的变更通知。不能把公式交给 WebView,而是亲自开发了布局引擎。动画也不能一直运行。所有动作都有开始和结束,结束后必须完全停止。
我们不会声称电池可以多续航几个小时。因为那是没有测量过的数字。我们确认过的只有帧数、CPU 使用率,以及线程被唤醒的次数。
无事发生时,什么也不做。
在 mote 中,这不是一项功能,而是所有功能都必须通过的原则。