Investigación

Mantener estable la app de Cursor

Andrew Chan & Kevin Nguyen9 min de lectura

Muchos de nuestros usuarios pasan todo el día usando Cursor, lo que significa que incluso los fallos poco frecuentes pueden resultar extremadamente disruptivos. Al mismo tiempo, el reto de mantener estable la app ha crecido a medida que hemos agregado usuarios y lanzado funcionalidades cada vez más ambiciosas como subagentes, instant grep, browser use y mucho más.

La mayoría de estos fallos se deben a que la app se queda sin memoria (OOM). En los últimos meses, hemos implementado sistemas para tener observabilidad sobre los fallos y la presión de memoria, aplicar soluciones y optimizaciones de alta confianza en rutas críticas, y añadir mecanismos de protección para detectar regresiones antes de que se lancen.

Nuestra tasa de OOM por sesión, agregada en todas las versiones de la app de Cursor, ha caído un 80 % desde su pico de finales de febrero, mientras que la tasa de OOM por solicitud ha bajado un 73 % desde el 1 de marzo. En esta publicación detallamos los sistemas que desarrollamos para conseguirlo.

Tasa de OOM por sesión a lo largo del tiempo, con una caída del 80 % desde finales de febreroTasa de OOM por sesión a lo largo del tiempo, con una caída del 80 % desde finales de febrero

Detección y medición de la inestabilidad

Nuestra aplicación de escritorio se basa en los cimientos de código abierto de Visual Studio Code y Electron, lo que le da una arquitectura multiproceso. Esto significa que pueden producirse fallos tanto en los procesos de renderizado que hacen funcionar el editor y la nueva ventana de agentes como en los procesos de utilidad que sustentan las extensiones, el almacenamiento y la funcionalidad del agente.

Los fallos en los procesos de renderizado son los más graves porque impiden por completo que el usuario use el editor. Hemos comprobado que, en su mayoría, se deben a los límites de memoria de V8 y son el foco principal de nuestros esfuerzos más recientes. Los fallos de las extensiones también pueden interrumpir funcionalidades importantes, como los servicios de lenguaje, pero normalmente se recuperan sin afectar tanto al usuario.

Nuestra telemetría registra cada fallo fatal junto con información contextual, como el proceso afectado, el tipo de fallo, los metadatos del dispositivo y de la aplicación, y minidumps y trazas de pila cuando están disponibles.

A partir de estos eventos de fallo, hemos creado métricas que podemos desglosar por versión de la aplicación y calcular por sesión o por solicitud; la primera refleja aproximadamente cuántas sesiones experimentan fallos, y la segunda, qué tan grave es el problema para las sesiones afectadas. Estos paneles se actualizan a los pocos minutos de producirse los fallos, por lo que podemos seguir de cerca los lanzamientos de nuevas versiones y detectar rápidamente posibles regresiones.

Fallos OOM a lo largo del tiempo, mostrando una disminución del 73 % desde el 1 de marzoFallos OOM a lo largo del tiempo, mostrando una disminución del 73 % desde el 1 de marzo

Dos estrategias de depuración

Seguimos una estrategia de dos frentes para depurar los cierres inesperados de la aplicación y los problemas de memoria insuficiente.

Top-down

La primera es una investigación top-down centrada en las funcionalidades que más memoria consumen. Si se sabe que una funcionalidad consume mucha memoria, podemos vincular las métricas de fallos con el feature flag correspondiente en Statsig, nuestra plataforma de experimentación, y luego hacer una prueba A/B para medir su contribución a las tasas de fallos.

También podemos hacer seguimiento de métricas indirectas que se correlacionan fuertemente con los fallos y que pueden ser más fáciles de observar durante el desarrollo. Una de esas métricas son los payloads de mensajes demasiado grandes. Debido a que nuestra aplicación usa una arquitectura multiproceso, los datos se transfieren constantemente entre el editor, las extensiones y los agentes a través de canales entre procesos y una capa de persistencia. Instrumentamos ambos para hacer seguimiento de los mensajes que superan cierto umbral, lo que se correlaciona fuertemente con los problemas de memoria, y adjuntamos pilas de llamadas para poder rastrear cada uno hasta su origen en el código de nuestra aplicación.

Para reconstruir lo que ocurre en el momento de un fallo específico, agregamos breadcrumbs (registros de metadatos especiales adjuntos a los errores) para funcionalidades como el uso paralelo de agentes, las llamadas a herramienta y las terminales, de modo que cada evento de fallo lleve un registro de la actividad que lo precedió.

bottom-up

En las investigaciones bottom-up rastreamos eventos de fallo individuales hasta su causa raíz. El primer paso es capturar qué ocurrió en el momento en que terminó el proceso. Ejecutamos un servicio de monitorización de fallos en el proceso principal que usa el protocolo Chrome DevTools (CDP) para detectar errores por falta de memoria y capturar pilas de fallo en tiempo real, y hemos parcheado Electron upstream para poder obtener estas pilas sin toda la pesada infraestructura de CDP. Estas pilas de fallo alimentan una automatización que se ejecuta a diario, analiza cada pila en detalle, crea PRs con optimizaciones para las pilas con correcciones de alta confianza y verifica la resolución de incidencias de una versión a otra.

Para entender cómo se acumula la memoria a lo largo de una sesión, observamos capturas del montón. Cuando detectamos que Cursor está usando demasiada memoria, pedimos al usuario que capture y envíe una. Estas capturas pueden contener información sensible, como el contenido de editores o chats abiertos, por lo que enviarlas es totalmente opcional. Pero son muy valiosas para rastrear la acumulación de presión de memoria hasta objetos y retenedores específicos, así que agradecemos mucho que los usuarios decidan participar.

Herramienta de captura del montón en Cursor que muestra retenedores de memoriaHerramienta de captura del montón en Cursor que muestra retenedores de memoria

Para entender los patrones de uso de memoria en toda la base de usuarios, ejecutamos un perfilado continuo de asignación del montón con una tasa de muestreo baja. Agregamos estos datos por versión de la aplicación para crear un desglose de la presión de memoria por pila de llamadas. Esto nos da una visión general de la presión de memoria a lo largo de las sesiones de la aplicación, e incluso podemos comparar diferencias entre versiones para entender si una ruta de asignación concreta en una versión más reciente de la aplicación mejoró o empeoró con respecto a las anteriores, y en qué medida.

Mitigaciones específicas

Con estos dos métodos de investigación, hemos visto que los fallos suelen encajar en uno de dos patrones.

El primero son los OOM agudos, en los que el uso de memoria se dispara de repente y el proceso se cae. Normalmente se detectan mediante trazas de fallo y rara vez aparecen en heapdumps o perfiles continuos. Una causa muy común es cuando una funcionalidad carga demasiados datos a la vez: nuestra aplicación trabaja mucho con el contenido de los espacios de trabajo de los usuarios y, por eso, a menudo carga el contenido completo de archivos desde el disco o a través de IPC. Hemos visto que algunos espacios de trabajo de usuarios pueden contener archivos enormes con los que la aplicación se atraganta, y ha sido fundamental agregar killswitches o dividir el procesamiento de blobs grandes en varios fragmentos.

El segundo son los OOM lentos y graduales, en los que el uso de memoria va aumentando a lo largo de una sesión hasta que empuja al proceso más allá del límite. Esto ocurre cuando el estado gestionado manualmente no se libera correctamente o cuando, por otros motivos, se filtran recursos a través de referencias fuertes residuales. Aparecen de forma fiable en los volcados de memoria y pueden solucionarse localizando qué los retiene y limpiando el ciclo de vida de los objetos de larga duración. Ya hemos enviado a VSCode algunas correcciones de fugas upstream y queremos agregar más.

Los fallos de las extensiones también pueden deberse a quedarse sin memoria, algo que mitigamos en parte mediante el aislamiento de procesos. En líneas generales, al ejecutar las extensiones en sus propios procesos aislados, evitamos que un fallo o una tarea prolongada en una extensión afecte a la funcionalidad de otra. Esto es similar a cómo Chrome aísla unas pestañas de otras, a costa de usar un poco más de memoria del sistema.

Evitar regresiones sin perder velocidad

Solucionar cierres inesperados de la aplicación suele ser más sencillo que evitar que se introduzcan nuevos, porque las soluciones son específicas. La prevención requiere que todos los desarrolladores sean conscientes de su impacto en la estabilidad sin sacrificar la velocidad que hemos ganado con agentes, lo que implica invertir tanto en procesos como en herramientas.

Algunas de las formas en que estamos abordando esto incluyen:

  • Reglas de Bugbot para cada una de las principales clases de OOM o cierres inesperados de la aplicación con las que nos hemos encontrado
  • Skills que nos permiten someter fácilmente nuestra aplicación a pruebas de estrés mediante el uso del ordenador con agentes
  • Eliminar fuentes habituales de errores, como sustituir recursos gestionados manualmente por recolección de basura para evitar fugas
  • Pruebas de rendimiento automatizadas tradicionales que se ejecutan después de cada cambio de código
  • Cerrar el ciclo de detección con métodos como reversiones automatizadas ante regresiones en las métricas

Estabilidad para una nueva generación de software

El desarrollo de software con agentes hace más fácil que nunca lanzar nuevas funcionalidades, pero también introducir problemas de rendimiento y errores. Al mismo tiempo, lograr la estabilidad de las aplicaciones sigue requiriendo los mismos fundamentos de la ingeniería de software, aunque adaptados a una nueva generación mediante estrategias con agentes para solucionar y prevenir incidencias.

Crear software de alta calidad siempre ha sido difícil, y ahora es más importante que nunca. Si es algo que te apasiona, nos encantaría saber de ti.