Forschung

Die Cursor-App stabil halten

Andrew Chan & Kevin Nguyen8 Min. Lesezeit

Viele unserer Benutzer verbringen ihren ganzen Tag damit, Cursor zu nutzen, was bedeutet, dass selbst seltene Abstürze extrem störend sein können. Gleichzeitig ist es schwieriger geworden, die App stabil zu halten, da sowohl die Zahl der Benutzer gestiegen ist als auch wir immer ambitioniertere Features wie Subagents, Instant Grep, Browser Use und mehr ausgeliefert haben.

Die meisten dieser Abstürze entstehen dadurch, dass der App der Speicher ausgeht (OOM). In den vergangenen Monaten haben wir Systeme implementiert, die uns mehr Transparenz über Abstürze und Speicherdruck geben, zuverlässige Fixes und Optimierungen für Hot Paths ermöglichen und Schutzmechanismen bereitstellen, die Regressionen erkennen, bevor sie ausgeliefert werden.

Unsere OOM-pro-Sitzung-Rate, aggregiert über alle Versionen der Cursor-App hinweg, ist seit ihrem Höchststand Ende Februar um 80 % gesunken, während OOMs pro Anfrage seit dem 1. März um 73 % zurückgegangen sind. In diesem Beitrag stellen wir die Systeme vor, mit denen wir das erreicht haben.

OOM pro Sitzung im Zeitverlauf mit einem Rückgang von 80 % seit Ende FebruarOOM pro Sitzung im Zeitverlauf mit einem Rückgang von 80 % seit Ende Februar

Instabilität erkennen und messen

Unsere Desktop-App basiert auf den Open-Source-Grundlagen von Visual Studio Code und Electron und hat daher eine Multiprozess-Architektur. Das bedeutet, dass Abstürze entweder in den Renderer-Prozessen auftreten können, die den Editor und das neue Agents-Fenster betreiben, oder in den Utility-Prozessen, die Erweiterungen, Speicher und Agent-Funktionen unterstützen.

Abstürze der Renderer-Prozesse sind am schwerwiegendsten, weil sie Benutzer vollständig daran hindern, den Editor zu nutzen. Nach unseren Erkenntnissen werden sie meist dadurch verursacht, dass V8-Speicherlimits erreicht werden, und stehen daher im Fokus unserer jüngsten Bemühungen. Abstürze von Erweiterungen können ebenfalls wichtige Funktionen wie Sprachdienste beeinträchtigen, erholen sich aber in der Regel, ohne Benutzer ähnlich stark zu stören.

Jeder fatale Absturz wird von unserer Telemetrie zusammen mit Kontextinformationen gemeldet, etwa zum betroffenen Prozess, zur Art des Absturzes, zu Geräte- und Anwendungsmetadaten sowie zu Minidumps und Stacktraces, sofern verfügbar.

Auf Grundlage dieser Absturz-Events haben wir Metriken erstellt, die wir nach App-Version aufschlüsseln können. Dabei berechnen wir Raten pro Sitzung oder pro Anfrage: Ersteres erfasst grob, wie viele Sitzungen Abstürze erleben, Letzteres, wie schwerwiegend das Absturzproblem für betroffene Sitzungen ist. Diese Dashboards werden innerhalb weniger Minuten nach Absturz-Events aktualisiert, sodass wir Releases neuer Versionen eng verfolgen und potenzielle Regressionen schnell erkennen können.

OOM-Abstürze im Zeitverlauf, mit einem Rückgang von 73 % seit dem 1. MärzOOM-Abstürze im Zeitverlauf, mit einem Rückgang von 73 % seit dem 1. März

Zweigleisige Debugging-Strategien

Beim Debugging von App-Abstürzen und Out-of-Memory-Problemen verfolgen wir einen zweigleisigen Ansatz.

Top-down

Die erste ist eine Top-down-Untersuchung, die sich auf die speicherintensivsten Features konzentriert. Wenn bekannt ist, dass ein Feature viel Speicher beansprucht, können wir Absturzmetriken mit dem entsprechenden Feature-Flag in Statsig, unserer Experimentierplattform, verknüpfen und anschließend per A/B-Test messen, wie stark es zu den Absturzraten beiträgt.

Wir können auch Proxy-Metriken erfassen, die stark mit Abstürzen korrelieren und sich in der Entwicklung möglicherweise leichter beobachten lassen. Eine solche Metrik sind übergroße Nachrichten-Payloads. Da unsere App eine Multiprozess-Architektur nutzt, werden ständig Daten zwischen dem Editor, Erweiterungen und Agenten über Interprozess-Kanäle und eine Persistenzschicht ausgetauscht. Wir instrumentieren beides, um Nachrichten zu erfassen, die einen bestimmten Schwellenwert überschreiten, was stark mit Speicherproblemen korreliert, und hängen Callstacks an, damit wir jede einzelne bis zu ihrer Quelle in unserem Anwendungscode zurückverfolgen können.

Um zu rekonstruieren, was im Moment eines bestimmten Absturzes passiert, fügen wir für Features wie parallele Agent-Nutzung, Tool-Aufrufe und Terminals Breadcrumbs hinzu (spezielle Metadaten-Logs, die an Fehler angehängt werden), sodass jedes Absturz-Event eine Aufzeichnung der vorausgegangenen Aktivität enthält.

Bottom-up

Bei Bottom-up-Untersuchungen verfolgen wir einzelne Absturz-Events bis zu ihrer eigentlichen Ursache zurück. Der erste Schritt ist, festzuhalten, was in dem Moment passiert ist, als der Prozess beendet wurde. Wir betreiben im Hauptprozess einen Crash-Watcher-Service, der das Chrome DevTools Protocol (CDP) nutzt, um Out-of-Memory-Fehler zu erkennen und Crash-Stacks in Echtzeit zu erfassen, und haben Electron Upstream gepatcht, damit sich diese Stacks auch ohne die schwergewichtige CDP-Infrastruktur abrufen lassen. Diese Crash-Stacks fließen in eine Automatisierung ein, die täglich läuft, jeden Stack im Detail analysiert, PRs mit Optimierungen für Stacks mit Fixes hoher Sicherheit erstellt und die Behebung von Issues von Version zu Version verifiziert.

Um zu verstehen, wie sich Speicher im Verlauf einer Sitzung ansammelt, betrachten wir Heap-Snapshots. Wenn wir erkennen, dass Cursor zu viel Speicher nutzt, bitten wir den Benutzer, einen Snapshot zu erstellen und zu senden. Diese Snapshots können sensible Informationen enthalten, etwa Inhalte aus geöffneten Editoren oder Chats, daher ist das Senden vollständig freiwillig. Gleichzeitig sind sie äußerst wertvoll, um den Aufbau von Speicherdruck bis zu bestimmten Objekten und Retainern zurückzuverfolgen, weshalb wir es sehr schätzen, wenn Benutzer sich dafür entscheiden, daran teilzunehmen.

Heap-Snapshot-Tool in Cursor mit Speicher-RetainernHeap-Snapshot-Tool in Cursor mit Speicher-Retainern

Um Nutzungsmuster beim Speicherverbrauch über die gesamte Benutzerbasis hinweg zu verstehen, führen wir kontinuierliches Heap-Allocation-Profiling mit einer niedrigen Sampling-Rate aus. Wir aggregieren diese Daten pro App-Version, um eine Aufschlüsselung des Speicherdrucks nach Callstack zu erstellen. So erhalten wir einen Überblick aus der Vogelperspektive über den Speicherdruck über App-Sitzungen hinweg, und wir können sogar Diffs zwischen Versionen erstellen, um zu verstehen, ob sich ein bestimmter Allocation-Path in einer neueren App-Version im Vergleich zu früheren Versionen verbessert oder verschlechtert hat und um wie viel.

Gezielte Gegenmaßnahmen

Mit diesen beiden Untersuchungsmethoden haben wir festgestellt, dass Abstürze im Allgemeinen in eines von zwei Mustern fallen.

Das erste sind akute OOMs, bei denen der Speicherverbrauch plötzlich sprunghaft ansteigt und der Prozess abstürzt. Diese lassen sich typischerweise anhand von Crash-Stacks finden und tauchen nur selten in Heap-Dumps oder kontinuierlichen Profilen auf. Eine sehr häufige Ursache ist, dass ein Feature zu viele Daten auf einmal lädt, was passieren kann, weil unsere App intensiv mit den Inhalten von Benutzer-Workspaces arbeitet und daher häufig vollständige Dateiinhalte von der Festplatte oder über IPC lädt. Wir haben gesehen, dass manche Benutzer-Workspaces riesige Dateien enthalten können, an denen die App scheitert. Deshalb war es entscheidend, Killswitches hinzuzufügen oder die Verarbeitung großer Blobs in mehrere Teile aufzuteilen.

Das zweite sind schleichende OOMs, bei denen der Speicherverbrauch im Verlauf einer Sitzung nach und nach steigt, bis der Prozess das Limit überschreitet. Diese treten auf, wenn manuell verwalteter Zustand nicht ordnungsgemäß freigegeben wird oder wenn wir anderweitig Ressourcen durch verbliebene starke Referenzen verlieren. Sie zeigen sich zuverlässig in Heap-Dumps und lassen sich beheben, indem man die Retainer aufspürt und den Lebenszyklus langlebiger Objekte bereinigt. Wir haben bereits einige Fixes für Speicherlecks in VSCode eingebracht und wollen weitere hinzufügen.

Abstürze von Erweiterungen können ebenfalls durch Speichermangel verursacht werden, was wir teilweise durch Prozessisolierung abmildern. Vereinfacht gesagt verhindern wir, dass ein Absturz oder eine lang laufende Aufgabe in einer Erweiterung die Funktionalität einer anderen beeinträchtigt, indem Erweiterungen in eigenen isolierten Prozessen ausgeführt werden. Das ähnelt der Art, wie Chrome Tabs voneinander isoliert, kostet allerdings etwas mehr Systemspeicher.

Regressionen verhindern, schnell bleiben

App-Abstürze zu beheben ist in der Regel einfacher, als zu verhindern, dass neue entstehen, weil Fixes gezielt ansetzen. Vorbeugung erfordert, dass jeder Entwickler die Auswirkungen seiner Änderungen auf die Stabilität versteht, ohne dabei das Tempo einzubüßen, das wir mit Agenten gewonnen haben. Das bedeutet, sowohl in Prozesse als auch in Tools zu investieren.

Einige unserer Ansätze dafür sind:

  • Bugbot-Regeln für jede größere Klasse von OOMs oder App-Abstürzen, auf die wir gestoßen sind
  • Skills, mit denen wir unsere Anwendung durch agentenbasierte Computerinteraktion einfach einem Stresstest unterziehen können
  • Fehlerquellen beseitigen, etwa indem wir manuell verwaltete Ressourcen durch Garbage Collection ersetzen, um Speicherlecks zu vermeiden
  • Traditionelle automatisierte Performance-Tests, die nach jeder Codeänderung ausgeführt werden
  • Den Regelkreis bei der Erkennung mit Methoden wie automatisierten Rollbacks bei Metrik-Regressionen zu schließen

Stabilität für eine neue Software-Generation

Agentenbasierte Softwareentwicklung macht es einfacher denn je, neue Features auszuliefern – und dabei zugleich Performance-Probleme und Bugs einzuführen. Gleichzeitig erfordert stabile Anwendungssoftware weiterhin dieselben Grundlagen des Software Engineering, allerdings weiterentwickelt für eine neue Generation: mit agentenbasierten Strategien, um Probleme zu beheben und zu vermeiden.

Hochwertige Software zu erstellen war schon immer schwierig, und heute ist es wichtiger denn je. Wenn dich dieses Thema begeistert, würden wir uns freuen, von dir zu hören.

Abgelegt unter: Forschung

Autors: Andrew Chan & Kevin Nguyen