研究

Cursorアプリの安定性を維持する

Andrew Chan & Kevin Nguyen読了時間 2分

多くのユーザーは、1日中Cursorを使っています。そのため、まれなクラッシュであっても非常に大きな支障になり得ます。一方で、サブエージェントinstant grepbrowser use など、ますます意欲的な機能を追加してリリースするなかで、アプリの安定性を維持する難しさも増してきました。

こうしたクラッシュの多くは、アプリがメモリ不足 (OOM) に陥ることが原因です。ここ数か月で私たちは、クラッシュやメモリ圧迫を可視化するための仕組み、ホットパスに対する確度の高い修正と最適化、そしてリグレッションをリリース前に検知するためのガードレールを実装してきました。

Cursorアプリの全バージョンを通して集計したセッションあたりのOOM発生率は、2月下旬のピークから80%低下しました。また、リクエストあたりのOOM発生率は、3月1日から73%低下しています。この投稿では、そこに至るまでに私たちが構築した仕組みを詳しく紹介します。

セッションあたりのOOM発生率の推移。2月下旬のピークから80%低下していることを示すセッションあたりのOOM発生率の推移。2月下旬のピークから80%低下していることを示す

不安定性の検出と測定

当社のデスクトップアプリは、Visual Studio Code と Electron というオープンソース技術を基盤としており、マルチプロセス構成を採用しています。そのため、エディタや新しいエージェントウィンドウを支えるレンダラープロセスだけでなく、拡張機能、ストレージ、エージェント機能を支えるユーティリティプロセスでもクラッシュが発生する可能性があります。

中でもレンダラープロセスのクラッシュは、ユーザーがエディタをまったく使えなくなるため、最も深刻です。これらの多くは V8 のメモリ上限に達することで発生していることがわかっており、直近の取り組みでも重点的に対処しています。拡張機能のクラッシュも、言語サービスのような重要な機能に影響を与えることがありますが、一般的にはユーザーへの影響をそれほど大きくせずに復旧します。

致命的なクラッシュはすべて、影響を受けたプロセス、クラッシュの種類、デバイスやアプリケーションのメタデータ、さらに利用可能な場合はミニダンプやスタックトレースといったコンテキストとともに、テレメトリで報告されます。

こうしたクラッシュイベントをもとに、当社ではアプリのバージョン別に内訳を確認できるメトリクスを構築しており、セッション単位またはリクエスト単位で発生率を算出しています。前者は、どれくらいのセッションでクラッシュが発生しているかを大まかに示し、後者は影響を受けたセッションにとってクラッシュ問題がどれほど深刻かを示します。これらのダッシュボードはクラッシュイベントから数分以内に更新されるため、新バージョンのリリース状況を綿密に確認し、潜在的なリグレッションをすばやく検出できます。

時間の経過に伴う OOM クラッシュ。3 月 1 日以降 73% 減少していることを示す時間の経過に伴う OOM クラッシュ。3 月 1 日以降 73% 減少していることを示す

2つのデバッグ戦略

アプリのクラッシュやメモリ不足の問題に対処するため、2つのアプローチを採用しています。

トップダウン

1つ目は、メモリ消費の大きい機能に焦点を当てるトップダウン型の調査です。ある機能がメモリを多く消費することが分かっていれば、実験プラットフォームである Statsig の対応する機能フラグにクラッシュのメトリクスを紐付けたうえで、A/B テストを実施し、クラッシュ率への寄与を測定できます。

また、クラッシュと強い相関があり、開発環境ではより観測しやすい代理メトリクスを確認することもできます。その1つが oversize message payloads です。私たちのアプリはマルチプロセス構成を採用しているため、データはプロセス間チャネルと永続化レイヤーを通じて、エディタ、拡張機能、エージェントの間で絶えず受け渡されています。Cursor では、その両方を計測対象として、一定のしきい値を超えるメッセージを追跡し、さらにコールスタックを付与することで、それぞれをアプリケーションコード内の発生元までたどれるようにしています。これはメモリの問題と強く相関します。

特定のクラッシュが起きた瞬間に何が起きていたのかを再構築するため、エージェントの並列利用、ツール呼び出し、ターミナルなどの機能について breadcrumbs (エラーに付随する特別なメタデータログ) を追加し、各クラッシュイベントに、その前に発生していたアクティビティの記録が含まれるようにしています。

ボトムアップ

ボトムアップの調査では、個々のクラッシュイベントを根本原因までさかのぼって追跡します。最初のステップは、プロセスが終了した瞬間に何が起きたのかを捉えることです。メインプロセスでクラッシュ監視サービスを実行し、Chrome DevTools Protocol (CDP) を使ってメモリ不足エラーを検出するとともに、クラッシュスタックをリアルタイムで取得しています。さらに、こうしたスタックを重量級の CDP の仕組みなしで取得できるようにするため、Electron の upstream にパッチも入れています。これらのクラッシュスタックは、毎日実行されるautomationに渡され、各スタックを詳しく分析し、高い確度で修正できるものについては最適化の PR を作成し、Issue の解決状況をバージョンごとに検証しています。

セッションを通してメモリがどのように蓄積していくのかを理解するために、ヒープスナップショットを確認します。Cursor がメモリを使いすぎていることを検出した場合、ユーザーにスナップショットを取得して送信するよう促します。これらのスナップショットには、開いているエディタやチャットの内容など、機密性の高い情報が含まれる可能性があるため、送信は完全にオプトインです。一方で、メモリ圧迫の蓄積を特定のオブジェクトやリテイナーまでさかのぼって追跡するうえで非常に価値が高いため、協力してくださるユーザーには感謝しています。

メモリリテイナーを示す Cursor のヒープスナップショットツールメモリリテイナーを示す Cursor のヒープスナップショットツール

ユーザー全体でのメモリ使用量の傾向を把握するために、低いサンプリングレートで継続的にヒープ割り当てプロファイリングを実行しています。このデータをアプリのバージョンごとに集約し、コールスタック別にメモリ圧迫の内訳を構築します。これにより、アプリのセッション全体にわたるメモリ圧迫を俯瞰して把握できるほか、バージョン間で diff を取ることで、新しいバージョンの特定の割り当てパスが以前と比べて改善したのか、それとも悪化したのか、さらにどの程度変化したのかまで理解できます。

的を絞った緩和策

この2つの調査手法から、クラッシュは概ね2つのパターンのいずれかに分類できることがわかっています。

1つ目は突発的な OOM で、メモリ使用量が急増してプロセスが終了してしまうものです。これは通常、クラッシュスタックから見つかり、ヒープダンプや継続的なプロファイルにはほとんど現れません。よくある原因の1つは、ある機能が一度に大量のデータを読み込んでしまうことです。これは、私たちのアプリがユーザーのワークスペース内の内容を幅広く扱っているため、ディスクや IPC 経由でファイル全体の内容を読み込むことがよくあるからです。実際、一部のユーザーワークスペースにはアプリが処理しきれないほど巨大なファイルが含まれていることがあり、キルスイッチを追加したり、大きな blob の処理を複数のチャンクに分割したりすることが非常に重要でした。

2つ目はじわじわ進行する OOM で、セッションの間にメモリ使用量が徐々に増え続け、最終的にプロセスが上限を超えてしまうものです。これは、手動で管理している状態が適切に破棄されていない場合や、意図しない強参照によってリソースリークが発生している場合に起こります。こうした問題はヒープダンプには確実に現れるため、retainer を追跡し、長寿命オブジェクトのライフサイクルを適切に整理することで修正できます。すでに VSCode にいくつかのリーク修正修正を upstream しており、今後さらに追加していく予定です。

拡張機能のクラッシュも、メモリ不足が原因で発生することがありますが、これについてはプロセス分離によって一部を緩和しています。大まかに言えば、拡張機能をそれぞれ独立した隔離プロセスで実行することで、ある拡張機能で発生したクラッシュや長時間タスクが、別の拡張機能の機能に影響を及ぼさないようにしています。これは Chrome がタブ同士を分離しているのと似ていますが、その代わりにシステムメモリの使用量はわずかに増えます。

リグレッションを防ぎつつ、スピードを維持する

アプリのクラッシュ修正は、対処すべき箇所が明確なぶん、新たなクラッシュを未然に防ぐよりも一般に取り組みやすいものです。予防には、エージェントによって得られた開発スピードを損なうことなく、安定性への影響をすべての開発者が意識できるようにする必要があります。そのためには、プロセスとツールの両面への投資が欠かせません。

私たちは、たとえば次のような形でこの課題に取り組んでいます。

  • これまでに遭遇した主要な OOM やアプリクラッシュの各パターンに対応する Bugbot ルール
  • エージェント型のコンピュータ操作を通じて、アプリケーションに手軽にストレステストをかけられる スキル
  • リークを防ぐために手動管理のリソースをガベージコレクションに置き換えるなど、事故を招きやすい要因をなくすこと
  • コード変更のたびに実行する、従来型の自動パフォーマンステスト
  • 指標の悪化時に自動でロールバックするなど、検出後の対応までを閉じたループにすること

新しい世代のソフトウェアのための安定性

エージェント型ソフトウェア開発により、新機能のリリースはこれまで以上に容易になる一方で、パフォーマンス上のIssueやバグも生じやすくなっています。同時に、アプリケーションの安定性を実現するには、ソフトウェアエンジニアリングの基本が依然として欠かせません。ただしそれらも、Issueの修正と未然防止に向けたエージェント型の戦略によって、新しい世代に合わせて進化していく必要があります。

高品質なソフトウェアを構築することは、昔から難しいものでした。そして今、その重要性はかつてなく高まっています。もしこれに情熱をお持ちなら、ぜひお話を聞かせてください

カテゴリー: 研究

著者s: Andrew Chan & Kevin Nguyen