视频 · 前往原文观看我们的许多用户整天都在使用 Cursor,这意味着即使是很罕见的崩溃也可能造成极大的干扰。与此同时,随着用户数量的增加以及我们推出越来越雄心勃勃的功能(如子智能体、即时 grep、浏览器使用等),保持应用稳定性的挑战也随之增大。
这些崩溃大多是由应用内存耗尽(OOM)引起的。过去几个月里,我们实施了多项系统,以提供对崩溃和内存压力的可观测性、对热点路径进行高置信度的修复和优化,并设置防护栏以在回归问题发布前将其捕获。
自 2 月底达到峰值以来,Cursor 应用所有版本汇总的每次会话 OOM 率已下降 80%,而自 3 月 1 日以来,每次请求的 OOM 率已下降 73%。本文详细介绍了我们为实现这一目标而构建的系统。
检测和衡量不稳定性
我们的桌面应用构建在 Visual Studio Code 和 Electron 的开源基础之上,这使其具有多进程架构。这意味着崩溃可能发生在渲染进程(驱动编辑器和新的智能体窗口)或实用进程(驱动扩展、存储和智能体功能)中。
渲染进程崩溃最为严重,因为它们会完全阻止用户使用编辑器。我们发现这些崩溃主要是由达到 V8 内存限制引起的,这也是我们近期工作的重点。扩展崩溃也可能中断语言服务等重要功能,但通常能在不严重干扰用户的情况下恢复。
每次致命崩溃都会通过我们的遥测系统报告,并附带相关上下文,例如受影响的进程、崩溃类型、设备和应用元数据,以及可用的 minidump 和堆栈跟踪信息。
根据这些崩溃事件,我们构建了可按应用版本细分的指标,以每次会话或每次请求为基础计算比率。前者大致衡量有多少会话经历了崩溃,后者则衡量受影响会话中崩溃问题的严重程度。这些仪表板在崩溃事件发生后的几分钟内更新,因此我们能够密切跟踪新版本的发布,并快速检测潜在的回归问题。
双重调试策略
我们采取双管齐下的策略来调试应用崩溃和内存不足问题。
自上而下
首先是自上而下的调查,重点关注内存消耗最大的功能。如果某个功能已知会消耗大量内存,我们可以将崩溃指标关联到实验平台 Statsig 中对应的功能开关,然后通过 A/B 测试来衡量该功能对崩溃率的贡献程度。
我们还可以跟踪与崩溃高度相关且更易于在开发过程中观察的代理指标。其中一个指标是过大的消息负载。由于我们的应用采用多进程架构,数据通过进程间通道和持久化层在编辑器、扩展和智能体之间不断传递。我们对这两者进行检测,以跟踪超过某个阈值的大消息(这与内存问题高度相关),并附加调用堆栈,以便将每个消息追溯到应用代码中的源头。
为了还原特定崩溃发生时的现场情况,我们为并行智能体使用、工具调用和终端等功能添加面包屑(附加到错误上的特殊元数据日志),这样每个崩溃事件都会携带其发生前的活动记录。
自下而上
在自下而上的调查中,我们将单个崩溃事件追溯到其根本原因。第一步是捕获进程终止时发生的情况。我们在主进程中运行一个崩溃监控服务,该服务使用 Chrome DevTools 协议(CDP)来检测内存不足错误并实时捕获崩溃堆栈,并且我们已对上游 Electron 进行了补丁,使得无需依赖繁重的 CDP 机制也能获取这些堆栈。这些崩溃堆栈会输入到一个每日运行的自动化流程中,该流程会详细分析每个堆栈,为具有高置信度修复方案的堆栈创建包含优化代码的 PR,并逐版本验证问题的解决情况。
为了理解内存如何在一次会话过程中累积,我们查看堆快照。当检测到 Cursor 占用过多内存时,我们会提示用户捕获并发送一份堆快照。这些快照可能包含敏感信息,例如打开的编辑器或聊天内容,因此发送完全由用户自主选择。但它们对于追踪内存压力如何累积到特定对象和持有者上非常有价值,这也让我们对选择参与的用户心怀感激。
为了了解全体用户的内存使用模式,我们以较低的采样率持续运行堆分配分析。我们按应用版本汇总这些数据,从而按调用栈构建出内存压力的细分情况。这让我们能够从全局视角审视应用会话期间的内存压力,甚至可以对比不同版本之间的差异,以了解新版本中某个特定分配路径相比之前是改善了还是退步了,以及变化幅度有多大。
针对性缓解措施
通过这两种调查方法,我们发现崩溃通常符合两种模式之一。
第一种是急性内存耗尽,即内存突然飙升,进程随之终止。这类问题通常通过崩溃堆栈发现,很少出现在堆转储或持续分析中。一个非常常见的原因是某个功能一次性加载了过多数据,这可能是由于我们的应用大量处理用户工作区的内容,因此经常从磁盘或通过进程间通信加载完整的文件内容。我们发现,某些用户工作区可能包含应用难以处理的大型文件,因此添加终止开关或将大型数据块的处理拆分为多个小块变得至关重要。
第二种是缓慢而稳定的内存耗尽,即内存在一次会话过程中逐渐攀升,直至将进程推至极限。这类问题发生在手动管理的状态未被正确释放,或者我们通过零散的强引用泄漏资源时。它们会可靠地出现在堆转储中,可以通过追踪持有者并清理长生命周期对象的生命周期来修复。我们已经向 VSCode 上游提交了一些泄漏修复,并计划添加更多。
扩展程序崩溃也可能由内存耗尽引起,我们通过进程隔离在一定程度上缓解了这一问题。大致来说,将扩展程序运行在各自独立的进程中,可以防止某个扩展程序的崩溃或长时间任务影响其他扩展程序的功能。这与 Chrome 浏览器隔离不同标签页的方式类似,代价是略微增加系统内存占用。
防止性能退化,保持快速响应
修复应用崩溃通常比防止新崩溃出现更直接,因为修复是有针对性的。预防则需要让每位开发者都意识到自身工作对稳定性的影响,同时不牺牲我们通过智能体所获得的开发速度,这意味着要在流程和工具两方面进行投入。
我们采取的一些方法包括:
- 针对我们遇到过的每一类主要 OOM(内存溢出)或应用崩溃,制定 Bugbot 规则
- 通过智能体计算机使用能力,让我们能够轻松对应用程序进行压力测试的技能
- 消除易错点,例如用垃圾回收替代手动管理资源以避免内存泄漏
- 每次代码变更后运行的传统自动化性能测试
- 通过指标退化自动回滚等方法,形成检测闭环
新一代软件的稳定性
智能体软件开发既让发布新功能变得前所未有地容易,也让引入性能问题和缺陷变得同样容易。与此同时,实现应用稳定性需要同样的软件工程基础原理,但需要针对新一代软件进行演进,通过智能体策略来修复和预防问题。
构建高质量软件一直都很困难,而现在比以往任何时候都更加重要。如果你对此充满热情,我们期待你的加入。