研究

保持 Cursor 应用稳定

Andrew Chan & Kevin Nguyen2 分钟阅读

我们的许多用户一整天都在使用 Cursor,这意味着即使偶发崩溃,也会带来极大干扰。与此同时,随着用户规模增长,以及我们陆续推出越来越复杂的功能,例如 子智能体即时 grep浏览器使用 等,保持应用稳定也变得更具挑战性。

这些崩溃大多是由应用内存耗尽 (OOM) 引起的。在过去几个月里,我们建立了一套系统,让我们能够观测崩溃和内存压力,对热点路径进行高把握的修复和优化,并设置保护机制,以便在发布前及时发现回归。

自 2 月下旬达到峰值以来,汇总 Cursor 应用所有版本统计的每会话 OOM 率已下降 80%;与此同时,自 3 月 1 日以来,每请求 OOM 率下降了 73%。这篇文章详细介绍了我们为此构建的这些系统。

每会话 OOM 率随时间变化,自 2 月下旬以来下降了 80%每会话 OOM 率随时间变化,自 2 月下旬以来下降了 80%

检测和衡量稳定性问题

我们的桌面应用构建在 Visual Studio Code 和 Electron 的开源基础之上,因此采用了多进程架构。这意味着,崩溃既可能发生在支撑编辑器和新智能体窗口的渲染进程中,也可能发生在支撑扩展、存储和智能体功能的工具进程中。

渲染进程崩溃最为严重,因为它会让用户完全无法使用编辑器。我们发现,这类崩溃大多是由于触及 V8 内存限制导致的,也是我们近期重点投入解决的问题。扩展崩溃也可能影响语言服务等重要功能,但通常能够自行恢复,对用户的干扰相对较小。

每一次致命崩溃都会由我们的遥测系统上报,同时附带相关上下文信息,例如受影响的进程、崩溃类型、设备和应用元数据,以及在可用情况下提供的 minidump 和堆栈跟踪。

基于这些崩溃事件,我们构建了一套可按应用版本细分的指标,并按每会话或每请求计算崩溃率。前者大致反映有多少会话会遇到崩溃,后者则反映崩溃问题对受影响会话的严重程度。这些仪表盘会在崩溃事件发生后的几分钟内更新,因此我们能够紧密跟踪新版本发布,并快速发现潜在回归问题。

OOM 崩溃随时间变化,自 3 月 1 日以来下降 73%OOM 崩溃随时间变化,自 3 月 1 日以来下降 73%

双重调试策略

我们采用双管齐下的方法来调试应用崩溃和内存耗尽问题。

自上而下

第一种是自上而下的排查方法,重点关注最耗内存的功能。如果已知某项功能内存开销较大,我们就可以将崩溃指标与 Statsig (我们的实验平台) 中对应的功能开关关联起来,再通过 A/B 测试衡量它对崩溃率的影响。

我们还可以跟踪与崩溃高度相关、且在开发阶段可能更容易观测到的替代指标。其中一个这样的指标就是过大的消息负载。由于我们的应用采用多进程架构,数据会持续通过进程间通道和持久化层在编辑器、扩展和智能体之间传递。我们会同时对这两者进行埋点,以跟踪超过某个阈值的消息;这与内存问题高度相关,并且还会附加调用堆栈,以便我们能够追溯每一条消息在应用代码中的来源。

为了还原某次特定崩溃发生时的情况,我们会为并行使用智能体、工具调用和终端等功能添加面包屑 (附加到错误上的特殊元数据日志) ,这样每个崩溃事件都会携带一份此前活动的记录。

自下而上

在自下而上的排查中,我们会将单个崩溃事件一路追溯到根本原因。第一步,是捕捉进程终止那一刻到底发生了什么。我们在主进程中运行了一项崩溃监测服务,使用 Chrome DevTools Protocol (CDP) 检测内存不足错误,并实时捕获崩溃堆栈;同时,我们还向 Electron 上游提交了补丁,从而无需依赖笨重的 CDP 机制也能获取这些堆栈。这些崩溃堆栈会输入一个每天运行的自动化流程,对每个堆栈做详细分析,为那些可以高置信度修复的问题创建优化 PR,并逐个版本确认问题是否已经解决。

为了了解内存在一次会话中是如何逐步累积的,我们会查看堆快照。当我们检测到 Cursor 使用了过多内存时,会提示用户捕获并发送一份堆快照。这些快照可能包含敏感信息,例如打开的编辑器或聊天中的内容,因此发送完全采取用户自愿选择加入的方式。但它们对于将不断累积的内存压力追溯到具体对象及其保留链非常有价值,所以当用户愿意参与时,我们也十分感激。

Cursor 中显示内存保留链的堆快照分析工具Cursor 中显示内存保留链的堆快照分析工具

为了了解整个用户群体的内存用量模式,我们会以较低的采样率持续运行堆分配分析。我们按应用版本对这些数据进行聚合,以构建按调用栈划分的内存压力明细。这让我们能够从全局视角观察各个 app 会话中的内存压力,甚至还能对不同版本做 diff,了解较新的应用版本中某条特定分配路径相较于之前版本是有所改善还是出现退化,以及幅度有多大。

有针对性的缓解措施

通过这两种调查方法,我们发现,崩溃通常可归为两种模式之一。

第一种是急性 OOM,也就是内存突然飙升,随后进程崩溃。这类问题通常通过崩溃堆栈发现,很少会出现在堆转储或持续性能分析中。一个很常见的原因是某个功能一次性加载了过多数据,这可能是因为我们的应用会大量处理用户工作区中的内容,因此经常需要从磁盘或通过 IPC 加载完整的文件内容。我们发现,有些用户工作区中可能包含体积巨大的文件,导致应用难以承受,因此添加熔断机制,或将大型数据块拆分成多个分块处理,至关重要。

第二种是缓慢累积型 OOM,即在一次会话过程中,内存逐步攀升,直到把进程推过上限。这类问题通常发生在手动管理的状态没有被正确释放,或由于残留的强引用而导致资源泄漏时。它们会稳定地出现在堆转储中,通常可以通过跟踪保留链并清理长生命周期对象的生命周期管理来修复。我们已经向 VSCode 上游提交了几个泄漏修复 补丁,并计划继续添加更多。

扩展崩溃也可能由内存耗尽引起,我们部分通过进程隔离来缓解这一问题。大致来说,通过让扩展在各自独立的隔离进程中运行,我们可以防止某个扩展中的崩溃或长任务影响其他扩展的功能。这类似于 Chrome 将各个选项卡彼此隔离的方式,代价是会额外占用少量系统内存。

防止回归,保持高效

修复应用崩溃通常比避免引入新的崩溃更直接,因为修复往往是有针对性的。要做好预防,就需要让每位开发者都清楚自己对稳定性的影响,同时又不牺牲我们借助智能体获得的开发效率,这意味着我们必须同时投入流程和工具建设。

我们采取的一些做法包括:

  • 针对我们遇到的每一类主要 OOM 或应用崩溃,制定相应的 Bugbot 规则
  • 使用 技能,让我们能通过智能体操作计算机,轻松对应用进行压力测试
  • 消除容易埋坑的设计,例如用垃圾回收替代手动管理的资源,以避免泄漏
  • 在每次代码变更后运行传统的自动化性能测试
  • 通过指标回退时自动回滚等方法,完善检测闭环

新一代软件的稳定性

智能体驱动的软件开发 让交付新功能变得前所未有地容易,也同样更容易引入性能问题和缺陷。与此同时,要实现应用稳定性,依然离不开软件工程的基础知识,只不过面向新一代,这些基础也需要不断演进,并借助智能体驱动的策略来修复和预防问题。

构建高质量软件一直都很难,而现在这件事比以往任何时候都更重要。如果您对此充满热情,欢迎与我们联系

分类: 研究

作者s: Andrew Chan & Kevin Nguyen