recherche

Assurer la stabilité de l’application Cursor

Andrew Chan & Kevin Nguyen9 min de lecture

Nombre de nos utilisateurs passent toute leur journée dans l’application Cursor, ce qui signifie que même des plantages rares peuvent être extrêmement perturbants. En parallèle, le défi consistant à préserver la stabilité de l’application s’est accentué à mesure que notre base d’utilisateurs s’est élargie et que nous avons livré des fonctionnalités de plus en plus ambitieuses comme les sous-agents, Instant Grep, l’utilisation du navigateur, et bien plus encore.

La plupart de ces plantages sont dus à un manque de mémoire de l’application (OOM). Au cours des derniers mois, nous avons mis en place des systèmes nous offrant une meilleure visibilité sur les plantages et la pression mémoire, des correctifs et des optimisations fiables pour les chemins les plus sollicités, ainsi que des garde-fous pour détecter les régressions avant leur livraison.

Notre taux d’OOM par session, agrégé sur l’ensemble des versions de l’application Cursor, a chuté de 80 % depuis son pic de fin février, tandis que le taux d’OOM par requête a baissé de 73 % depuis le 1er mars. Cet article détaille les systèmes que nous avons construits pour y parvenir.

Taux d’OOM par session au fil du temps, montrant une baisse de 80 % depuis fin févrierTaux d’OOM par session au fil du temps, montrant une baisse de 80 % depuis fin février

Détection et mesure de l'instabilité

Notre application de bureau s'appuie sur les bases open source de Visual Studio Code et d'Electron, ce qui lui donne une architecture multiprocessus. Cela signifie que des plantages peuvent se produire aussi bien dans les processus de rendu, qui font fonctionner l'éditeur et la nouvelle fenêtre des agents, que dans les processus utilitaires, qui prennent en charge les extensions, le stockage et les fonctionnalités des agents.

Les plantages des processus de rendu sont les plus graves, car ils empêchent complètement l'utilisateur d'utiliser l'éditeur. Nous avons constaté qu'ils sont principalement dus au dépassement des limites de mémoire de V8, et c'est là que se concentrent l'essentiel de nos efforts récents. Les plantages d'extensions peuvent aussi perturber des fonctionnalités importantes, comme les services de langage, mais ils se résolvent généralement avec moins d'impact pour l'utilisateur.

Chaque plantage fatal est remonté par notre télémétrie, avec des informations de contexte comme le processus concerné, le type de plantage, les métadonnées de l'appareil et de l'application, ainsi que les minidumps et les traces de pile lorsqu'ils sont disponibles.

À partir de ces événements de plantage, nous avons mis en place des métriques que nous pouvons ventiler par version de l'application, en calculant des taux par session ou par requête. La première donne une estimation du nombre de sessions qui subissent des plantages, tandis que la seconde indique à quel point le problème est grave pour les sessions concernées. Ces tableaux de bord se mettent à jour quelques minutes après les événements de plantage, ce qui nous permet de suivre de près les sorties de nouvelles versions et de détecter rapidement d'éventuelles régressions.

Plantages OOM au fil du temps, montrant une baisse de 73 % depuis le 1er marsPlantages OOM au fil du temps, montrant une baisse de 73 % depuis le 1er mars

Deux stratégies de débogage

Nous appliquons une approche à deux volets pour déboguer les plantages d’application et les problèmes de mémoire insuffisante.

Descendante

La première est une investigation descendante axée sur les fonctionnalités les plus gourmandes en mémoire. Si une fonctionnalité est connue pour être gourmande en mémoire, nous pouvons relier les métriques de plantage au feature flag correspondant dans Statsig, notre plateforme d’expérimentation, puis effectuer un test A/B pour mesurer sa contribution au taux de plantage.

Nous pouvons également suivre des métriques proxy fortement corrélées aux plantages et parfois plus faciles à observer en développement. L’une de ces métriques concerne les charges utiles de messages surdimensionnées. Comme notre application utilise une architecture multiprocessus, des données transitent en permanence entre l’éditeur, les extensions et les agents via des canaux interprocessus et une couche de persistance. Nous instrumentons les deux pour suivre les messages dépassant un certain seuil, fortement corrélés aux problèmes de mémoire, et y associons des piles d’appels afin de pouvoir retracer chacun jusqu’à sa source dans le code de notre application.

Pour reconstituer ce qui se passe au moment d’un plantage précis, nous ajoutons des breadcrumbs (journaux de métadonnées spéciaux associés aux erreurs) pour des fonctionnalités comme l’utilisation parallèle d’agents, les appels d’outils et les terminaux, afin que chaque événement de plantage embarque une trace de l’activité qui l’a précédé.

Approche ascendante

Dans les investigations ascendantes, nous faisons remonter chaque événement de plantage jusqu’à sa cause racine. La première étape consiste à capturer ce qui s’est passé au moment où le processus s’est arrêté. Nous exécutons dans le processus principal un service de surveillance des plantages qui utilise le Chrome DevTools Protocol (CDP) pour détecter les erreurs de mémoire insuffisante et capturer les piles de plantage en temps réel, et avons apporté un correctif à Electron en amont afin de rendre possible l’obtention de ces piles sans recourir à la lourde infrastructure CDP. Ces piles de plantage alimentent une automatisation qui s’exécute chaque jour, analyse chaque pile en détail, crée des PR contenant des optimisations pour les piles dont les correctifs sont jugés très fiables, et vérifie la résolution des problèmes d’une version à l’autre.

Pour comprendre comment la mémoire s’accumule au fil d’une session, nous examinons des instantanés du tas. Lorsque nous détectons que Cursor utilise trop de mémoire, nous invitons l’utilisateur à en capturer un et à nous l’envoyer. Ces instantanés peuvent contenir des informations sensibles, comme le contenu d’éditeurs ou de chats ouverts ; leur envoi repose donc entièrement sur le volontariat. Mais ils sont extrêmement précieux pour remonter l’accumulation de pression mémoire jusqu’à des objets et rétenteurs spécifiques, ce qui nous rend particulièrement reconnaissants lorsque des utilisateurs choisissent de participer.

Outil d’instantané du tas dans Cursor montrant les rétenteurs mémoireOutil d’instantané du tas dans Cursor montrant les rétenteurs mémoire

Pour comprendre les patterns d’utilisation de la mémoire à l’échelle de l’ensemble des utilisateurs, nous exécutons en continu un profilage des allocations du tas à un faible taux d’échantillonnage. Nous agrégeons ces données par version de l’application afin d’établir une ventilation de la pression mémoire par pile d’appels. Cela nous donne une vue d’ensemble de la pression mémoire sur les sessions de l’application, et nous pouvons même faire des diff entre les versions pour déterminer si un chemin d’allocation donné, dans une version plus récente de l’application, s’est amélioré ou a régressé par rapport aux précédentes, et dans quelle mesure.

Mesures d'atténuation ciblées

Grâce à ces deux méthodes d'investigation, nous avons constaté que les plantages suivent généralement l'un de deux patterns.

Le premier correspond à des OOM aigus, où l'utilisation mémoire grimpe soudainement et le processus s'arrête. Ils sont généralement repérés via les piles de crash et apparaissent rarement dans les heap dumps ou les profils continus. Une cause très fréquente est qu'une fonctionnalité charge trop de données d'un seul coup, ce qui peut arriver parce que notre application manipule beaucoup le contenu des espaces de travail des utilisateurs et charge donc souvent l'intégralité des fichiers depuis le disque ou via IPC. Nous avons constaté que certains espaces de travail utilisateur peuvent contenir des fichiers énormes que l'application ne parvient pas à traiter correctement, et il a été crucial d'ajouter des killswitches ou de découper le traitement des gros blobs en plusieurs blocs.

Le second correspond à des OOM progressifs, où l'utilisation mémoire augmente lentement au fil d'une session jusqu'à faire dépasser la limite au processus. Ils se produisent lorsqu'un état géré manuellement n'est pas correctement libéré, ou lorsque des ressources fuient à cause de références fortes parasites. Ils apparaissent de manière fiable dans les heap dumps et peuvent être corrigés en identifiant ce qui les retient en mémoire et en nettoyant le cycle de vie des objets de longue durée. Nous avons déjà contribué en amont à quelques correctifs de fuite de mémoire dans VSCode et cherchons à en ajouter d'autres.

Les plantages des extensions peuvent aussi être causés par un manque de mémoire, que nous atténuons en partie grâce à l'isolation des processus. En pratique, en exécutant les extensions dans leurs propres processus isolés, nous empêchons qu'un plantage ou une tâche longue dans une extension n'affecte le fonctionnement d'une autre. C'est similaire à la manière dont Chrome isole les onglets les uns des autres, au prix d'une consommation de mémoire système légèrement plus élevée.

Prévenir les régressions sans ralentir

Corriger les crashs d’application est généralement plus simple que d’éviter d’en introduire de nouveaux, car les correctifs sont ciblés. La prévention suppose de sensibiliser chaque développeur à l’impact de ses changements sur la stabilité, sans sacrifier la vélocité gagnée grâce aux agents, ce qui implique d’investir à la fois dans les processus et les outils.

Voici quelques-unes de nos approches :

  • Des règles Bugbot pour chaque grande catégorie d’OOM ou de crash applicatif rencontrée
  • Des Skills qui nous permettent de soumettre facilement notre application à des tests de charge grâce à une utilisation agentique de l’ordinateur
  • Éliminer les pièges, par exemple en remplaçant les ressources gérées manuellement par le ramasse-miettes afin d’éviter les fuites
  • Des tests de performance automatisés traditionnels qui s’exécutent après chaque changement de code
  • Fermer la boucle de la détection avec des méthodes comme les retours en arrière automatiques en cas de régression des métriques

Stabilité pour une nouvelle génération de logiciels

Le développement logiciel agentique facilite plus que jamais aussi bien la livraison de nouvelles fonctionnalités que l’apparition de problèmes de performances et de bugs. En parallèle, assurer la stabilité des applications repose sur les mêmes fondamentaux de l’ingénierie logicielle, adaptés à cette nouvelle génération grâce à des stratégies agentiques de correction et de prévention des problèmes.

Créer des logiciels de haute qualité a toujours été difficile, et c’est plus important que jamais aujourd’hui. Si c’est un sujet qui vous passionne, nous serions ravis d’échanger avec vous.

Classé dans : recherche

Auteurs: Andrew Chan & Kevin Nguyen