研究

維持 Cursor 應用程式的穩定性

Andrew Chan & Kevin Nguyen閱讀時間 2 分鐘

許多使用者一整天都在使用 Cursor,這意味著即使是少見的當機,也可能造成極大干擾。與此同時,隨著我們加入更多使用者,並推出像是 subagentsinstant grepbrowser use 等愈來愈大膽的功能,維持應用程式穩定的挑戰也隨之增加。

這些當機大多是由應用程式記憶體不足 (OOM) 所造成。過去幾個月來,我們建置了多套系統,讓我們能觀測當機與記憶體壓力、針對熱點路徑進行高信心的修正與最佳化,並設下防護機制,在回歸問題正式推出前就先攔下。

自 2 月下旬的高峰以來,跨 Cursor 應用程式所有版本彙總的每工作階段 OOM 發生率已下降 80%;每請求 OOM 發生率自 3 月 1 日以來下降了 73%。這篇文章將詳細介紹我們為達成這些成果所建置的系統。

每個工作階段的 OOM 發生率隨時間變化,顯示自 2 月下旬以來下降 80%每個工作階段的 OOM 發生率隨時間變化,顯示自 2 月下旬以來下降 80%

偵測與衡量不穩定性

我們的桌面應用程式建立在 Visual Studio Code 和 Electron 的開源基礎上,因此採用多行程架構。這表示當機可能發生在支援編輯器和新版 Agent 視窗的渲染行程中,也可能發生在支援擴充功能、儲存與 Agent 功能的公用程式行程中。

渲染行程當機最為嚴重,因為這會讓使用者完全無法使用編輯器。我們發現,這類問題大多是因為觸及 V8 記憶體額度限制所致,也是我們近期工作的主要重點。擴充功能當機也可能中斷語言服務等重要功能,但通常能在較不影響使用者的情況下自行復原。

每一次致命當機都會由我們的遙測系統回報,並附帶上下文資訊,例如受影響的行程、當機類型、裝置與應用程式中繼資料,以及在可使用時提供的迷你傾印和堆疊追蹤。

根據這些當機事件,我們建立了可依應用程式版本細分的指標,並以每個工作階段或每次請求為單位計算比率;前者大致反映有多少工作階段會遇到當機,後者則反映當機問題對受影響工作階段而言有多嚴重。這些儀表板會在當機事件發生後幾分鐘內更新,因此我們能夠密切追蹤新版本的發布,並快速偵測潛在的回歸問題。

OOM 當機隨時間變化,自 3 月 1 日以來下降 73%OOM 當機隨時間變化,自 3 月 1 日以來下降 73%

雙管齊下的除錯策略

我們採取雙管齊下的方式,針對應用程式當機和記憶體不足問題進行除錯。

由上而下

第一種是由上而下的調查方式,重點放在最耗用記憶體的功能上。如果已知某項功能特別耗用記憶體,我們就可以把當機指標連結到 Statsig (我們的實驗平台) 中對應的功能旗標,再透過 A/B 測試衡量它對當機率的貢獻。

我們也可以追蹤與當機高度相關,且在開發期間可能更容易觀察到的替代指標。其中一項指標是過大的訊息酬載。由於我們的 app 採用多行程架構,資料會持續透過行程間通道與持久化層,在編輯器、擴充功能和 Agent 之間傳遞。我們會對兩者進行儀器化,追蹤大於某個門檻的訊息;這與記憶體問題高度相關,並附加呼叫堆疊,讓我們能將每一筆訊息一路追溯到應用程式程式碼中的來源。

為了重建特定當機發生當下的情況,我們會為平行 Agent 用量、工具呼叫和終端機等功能加入 breadcrumbs (附加在錯誤上的特殊中繼資料日誌) ,讓每個當機事件都帶有此前活動的記錄。

由下而上

在由下而上的調查中,我們會從個別的當機事件一路追查到根本原因。第一步是捕捉行程終止當下究竟發生了什麼事。我們在主行程中執行一項當機監看服務,使用 Chrome DevTools Protocol (CDP) 偵測記憶體不足錯誤,並即時擷取當機堆疊;同時也已對 Electron 上游做出修補,讓我們無須仰賴笨重的 CDP 機制也能取得這些堆疊。這些當機堆疊會送入一個自動化流程,該流程每天執行,詳細分析每個堆疊,針對可高信心修正的堆疊提出最佳化 PR,並逐版驗證議題是否已解決。

為了了解記憶體如何在一次工作階段中逐步累積,我們會查看堆積快照。當我們偵測到 Cursor 使用過多記憶體時,會提示使用者擷取並傳送一份。這些快照可能包含敏感資訊,例如開啟中的編輯器內容或聊天內容,因此是否傳送完全採自願加入。不過,它們對於將不斷累積的記憶體壓力追溯到特定物件及其 retainers 非常有價值,所以當使用者選擇參與時,我們都非常感激。

Cursor 中顯示記憶體 retainers 的堆積快照工具Cursor 中顯示記憶體 retainers 的堆積快照工具

為了了解整體使用者群的記憶體用量模式,我們會以低取樣率持續進行堆積配置剖析。我們會依 app 版本彙總這些資料,打造依呼叫堆疊拆分的記憶體壓力明細。這讓我們能從鳥瞰視角掌握各個 app 工作階段中的記憶體壓力,甚至還能比較版本間的差異,了解較新 app 版本中的特定配置路徑,相較於先前版本是有所改進還是退步,以及幅度有多大。

針對性的緩解措施

透過這兩種調查方法,我們發現,當機問題通常可歸為兩種模式之一。

第一種是急性 OOM,也就是記憶體突然暴增,接著行程終止。這類問題通常會從當機堆疊中發現,很少出現在 heap dump 或持續剖析資料中。一個很常見的原因是某項功能一次載入過多資料,而這可能是因為我們的應用程式會大量處理使用者工作區中的內容,因此經常會從磁碟或透過 IPC 載入完整檔案內容。我們觀察到,有些使用者工作區可能包含非常大的檔案,導致應用程式難以負荷,因此加入緊急停用開關,或將大型 blob 的處理拆成多個區塊,一直都是很關鍵的做法。

第二種是緩慢累積型 OOM,也就是記憶體在一次工作階段中逐步攀升,最後把行程推過上限。這類情況通常發生在手動管理的狀態未正確釋放,或因為零星殘留的強參考而造成資源洩漏。這些問題會穩定地出現在 heap dump 中,並可透過追查 retainers,以及整理長期存活物件的生命週期來修正。我們已經將一些記憶體洩漏 修正提交回 VSCode 上游,並希望加入更多。

擴充功能當機也可能是由記憶體耗盡造成,而我們部分是透過行程隔離來緩解。簡單來說,讓擴充功能在各自獨立隔離的行程中執行,可以避免某個擴充功能中的當機或長時間任務影響到另一個擴充功能的功能。這和 Chrome 將各個分頁彼此隔離的做法類似,但代價是會多占用一些系統記憶體。

避免回歸問題,同時維持速度

修正應用程式當機,通常比防止新的當機再次引入更直接,因為修正通常是針對特定問題。預防則需要讓每位開發者都意識到自己的變更對穩定性的影響,同時又不犧牲我們透過 Agent 獲得的開發速度,這也意味著我們必須同時投資於流程與工具。

我們目前採取的一些做法包括:

  • 針對我們遇過的每一種主要 OOM 或應用程式當機類型,建立對應的 Bugbot 規則
  • 使用 技能,讓我們能透過代理式的電腦操作,輕鬆對應用程式進行壓力測試
  • 消除容易誤用且暗藏風險的設計,例如以垃圾回收取代手動管理的資源,避免發生洩漏
  • 在每次程式碼變更後執行傳統的自動化效能測試
  • 透過指標回歸時自動回滾等方法,讓偵測機制形成閉環

新一代軟體的穩定性

代理式軟體開發 讓推出新功能變得前所未有地容易,但也更容易帶來效能問題和錯誤。同時,要達成應用程式的穩定性,依然需要軟體工程的基本功,只是必須隨著新世代演進,透過代理式策略來修正並預防問題。

打造高品質軟體向來都不容易,而現在更是比以往任何時候都重要。如果這正是你熱衷的事,我們很希望聽到你的消息

分類於: 研究

作者s: Andrew Chan & Kevin Nguyen