Cursor ऐप को स्थिर रखना
हमारे कई उपयोगकर्ता Cursor का इस्तेमाल पूरे दिन करते हैं, इसलिए कभी-कभार होने वाले क्रैश भी बेहद बाधक हो सकते हैं। साथ ही, ऐप को स्थिर बनाए रखने की चुनौती भी बढ़ी है, क्योंकि हमने अधिक उपयोगकर्ता जोड़े हैं और उप-एजेंट, Instant Grep, ब्राउज़र उपयोग जैसी लगातार अधिक महत्वाकांक्षी सुविधाएँ शिप की हैं, और भी बहुत कुछ।
इनमें से ज़्यादातर क्रैश ऐप में मेमोरी खत्म होने (OOM) की वजह से होते हैं। पिछले कुछ महीनों में, हमने ऐसे सिस्टम लागू किए हैं जिनसे हमें क्रैश और मेमोरी दबाव की बेहतर दृश्यता मिलती है, महत्वपूर्ण पाथ के लिए भरोसेमंद सुधार और ऑप्टिमाइज़ेशन मिलते हैं, और रिग्रेशन को शिप होने से पहले पकड़ने के लिए सुरक्षा उपाय मिलते हैं।
Cursor ऐप के सभी संस्करणों में समेकित हमारी प्रति-सत्र OOM दर फरवरी के आखिर में आए अपने शिखर के बाद से 80% घट गई है, जबकि प्रति-अनुरोध OOM दर 1 मार्च के बाद से 73% घटी है। इस पोस्ट में उन सिस्टमों का विवरण है जिन्हें हमने यहाँ तक पहुँचने के लिए बनाया।


अस्थिरता का पता लगाना और उसका आकलन
हमारा डेस्कटॉप ऐप Visual Studio Code और Electron की ओपन-सोर्स नींव पर बनाया गया है, जिससे इसमें मल्टी-प्रोसेस आर्किटेक्चर मिलता है। इसका मतलब है कि क्रैश या तो उन renderer प्रक्रियाओं में हो सकते हैं जो एडिटर और नई एजेंट्स विंडो को चलाती हैं, या उन utility प्रक्रियाओं में जो एक्सटेंशन, स्टोरेज और एजेंट की कार्यक्षमता को संभालती हैं।
Renderer क्रैश सबसे गंभीर होते हैं, क्योंकि वे उपयोगकर्ता को एडिटर इस्तेमाल करने से पूरी तरह रोक देते हैं। हमने पाया है कि ये ज़्यादातर V8 की मेमोरी सीमाओं तक पहुँच जाने के कारण होते हैं, और हाल के हमारे प्रयास मुख्य रूप से इन्हीं पर केंद्रित रहे हैं। एक्सटेंशन क्रैश भी लैंग्वेज सर्विसेज जैसी महत्वपूर्ण कार्यक्षमता को बाधित कर सकते हैं, लेकिन आम तौर पर वे उपयोगकर्ता को उतना प्रभावित किए बिना रिकवर हो जाते हैं।
हर fatal क्रैश को हमारी टेलीमेट्री रिपोर्ट करती है, साथ ही उससे जुड़ा संदर्भ भी दर्ज करती है, जैसे प्रभावित प्रक्रिया, क्रैश का प्रकार, डिवाइस और ऐप metadata, और जहाँ उपलब्ध हों वहाँ minidumps और stack traces।
इन क्रैश इवेंट्स से हमने ऐसे मेट्रिक्स बनाए हैं जिन्हें हम ऐप संस्करण के हिसाब से विभाजित कर सकते हैं, और प्रति-सत्र या प्रति-अनुरोध के आधार पर दरें निकाल सकते हैं। इनमें पहला मोटे तौर पर यह दिखाता है कि कितने सत्र क्रैश का अनुभव करते हैं, जबकि दूसरा यह बताता है कि प्रभावित सत्रों के लिए क्रैश की समस्या कितनी गंभीर है। ये डैशबोर्ड क्रैश इवेंट्स के कुछ ही मिनटों में अपडेट हो जाते हैं, इसलिए हम नए संस्करणों के रिलीज़ को क़रीब से ट्रैक कर पाते हैं और संभावित रिग्रेशन का जल्दी पता लगा लेते हैं।


डीबगिंग की दोहरी रणनीतियाँ
ऐप क्रैश और आउट-ऑफ-मेमोरी समस्याओं की डीबगिंग के लिए हम दोहरी रणनीति अपनाते हैं।
टॉप-डाउन
पहली रणनीति एक टॉप-डाउन जांच की है, जो सबसे अधिक मेमोरी-गहन सुविधाओं पर केंद्रित है। अगर किसी सुविधा के मेमोरी-गहन होने की जानकारी है, तो हम क्रैश मेट्रिक्स को Statsig, हमारे experimentation platform, में संबंधित feature flag से लिंक कर सकते हैं, और फिर A/B परीक्षण करके क्रैश दरों में उसके योगदान को माप सकते हैं।
हम ऐसे प्रॉक्सी मेट्रिक्स को भी ट्रैक कर सकते हैं, जिनका क्रैश से मज़बूत संबंध होता है और जिन्हें विकास के दौरान देखना अपेक्षाकृत आसान हो सकता है। ऐसा ही एक मेट्रिक है बहुत बड़े message payloads। चूँकि हमारा ऐप मल्टी-प्रोसेस आर्किटेक्चर का उपयोग करता है, डेटा लगातार एडिटर, एक्सटेंशन और एजेंट के बीच inter-process channels और एक persistence layer के ज़रिए भेजा जाता है। हम दोनों को instrument करते हैं, ताकि एक निश्चित सीमा से बड़े messages को ट्रैक किया जा सके, जिनका मेमोरी समस्याएँ से गहरा संबंध होता है, और उनके साथ कॉलस्टैक संलग्न करते हैं, ताकि हम हर मामले को अपने application code में उसके स्रोत तक ट्रेस कर सकें।
किसी विशिष्ट क्रैश के समय क्या हुआ, इसे फिर से समझने के लिए, हम parallel agent उपयोग, टूल कॉल्स और टर्मिनल जैसी सुविधाओं के लिए breadcrumbs (errors से जुड़े विशेष metadata logs) जोड़ते हैं, ताकि हर क्रैश इवेंट अपने पहले की गतिविधि का रिकॉर्ड साथ लेकर चले।
बॉटम-अप
बॉटम-अप जांच में हम अलग-अलग क्रैश इवेंट्स को उनके मूल कारण तक ट्रेस करते हैं। पहला चरण यह दर्ज करना है कि प्रक्रिया के बंद होने के ठीक उसी क्षण क्या हुआ। हम मुख्य प्रक्रिया में एक crash watcher सेवा चलाते हैं, जो Chrome DevTools Protocol (CDP) का इस्तेमाल करके आउट-ऑफ-मेमोरी त्रुटियों का पता लगाती है और रीयल टाइम में क्रैश स्टैक्स कैप्चर करती है। साथ ही, Electron upstream में पैच भी किया है, ताकि भारी-भरकम CDP मशीनरी के बिना भी ये स्टैक प्राप्त किए जा सकें। ये क्रैश स्टैक्स एक स्वचालन को फ़ीड करते हैं, जो हर दिन चलता है, हर स्टैक का विस्तार से विश्लेषण करता है, जिन स्टैक्स के लिए उच्च-विश्वास वाले सुधार उपलब्ध हों उनके लिए ऑप्टिमाइज़ेशन के साथ PRs बनाता है, और संस्करण-दर-संस्करण समस्या के समाधान को सत्यापित करता है।
यह समझने के लिए कि एक सत्र के दौरान मेमोरी कैसे जमा होती है, हम heap snapshots देखते हैं। जब हमें पता चलता है कि Cursor बहुत ज़्यादा मेमोरी उपयोग कर रहा है, तो हम उपयोगकर्ता को एक snapshot कैप्चर करके भेजने के लिए प्रॉम्प्ट करते हैं। इन snapshots में संवेदनशील जानकारी हो सकती है, जैसे खुले एडिटर या चैट की सामग्री, इसलिए इन्हें भेजना पूरी तरह opt-in है। लेकिन मेमोरी दबाव के बढ़ने को विशिष्ट ऑब्जेक्ट्स और रिटेनर्स तक ट्रेस करने में ये बेहद मूल्यवान होते हैं, इसलिए जब उपयोगकर्ता इसमें भाग लेना चुनते हैं, तो हम उसकी सराहना करते हैं।


पूरे उपयोगकर्ता आधार में मेमोरी उपयोग के पैटर्न समझने के लिए, हम कम sampling rate पर सतत हीप आवंटन प्रोफाइलिंग चलाते हैं। हम इस डेटा को हर ऐप संस्करण के अनुसार समेकित करते हैं, ताकि कॉलस्टैक के आधार पर मेमोरी दबाव का एक विभाजन तैयार किया जा सके। इससे हमें ऐप सत्रों में मेमोरी दबाव का एक समग्र दृश्य मिलता है। हम संस्करणों के बीच डिफ भी कर सकते हैं, ताकि यह समझ सकें कि किसी नए ऐप संस्करण में कोई विशेष allocation path पिछले संस्करणों की तुलना में बेहतर हुआ है या खराब, और कितना।
लक्षित शमन उपाय
जांच के इन दो तरीकों से, हमने पाया है कि क्रैश आम तौर पर दो पैटर्न में से किसी एक में आते हैं।
पहला है तीव्र OOM, जिसमें मेमोरी अचानक बहुत बढ़ जाती है और प्रक्रिया बंद हो जाती है। ये आम तौर पर क्रैश स्टैक्स के ज़रिए मिलते हैं और हीप डंप्स या निरंतर प्रोफ़ाइलों में बहुत कम दिखाई देते हैं। इसका एक बहुत सामान्य कारण यह है कि कोई सुविधा एक साथ बहुत ज़्यादा डेटा लोड कर लेती है। ऐसा इसलिए हो सकता है क्योंकि हमारा ऐप उपयोगकर्ताओं के कार्यस्थानों की सामग्री के साथ बड़े पैमाने पर काम करता है, और अक्सर डिस्क से या IPC के ज़रिए पूरी फ़ाइल की सामग्री लोड कर लेता है। हमने देखा है कि कुछ उपयोगकर्ता कार्यस्थानों में बहुत बड़ी फ़ाइलें हो सकती हैं, जिन पर ऐप अटक जाता है, और ऐसे में killswitches जोड़ना या बड़े blobs के प्रसंस्करण को कई हिस्सों में बाँटना बेहद महत्वपूर्ण रहा है।
दूसरा है धीमा-और-स्थिर OOM, जिसमें एक सत्र के दौरान मेमोरी धीरे-धीरे बढ़ती रहती है, जब तक कि वह प्रक्रिया को उसकी सीमा से आगे न धकेल दे। ऐसा तब होता है जब मैन्युअल रूप से प्रबंधित state का सही तरीके से निपटान नहीं होता, या जब stray strong references की वजह से संसाधन लीक हो जाते हैं। ये हीप डंप्स में भरोसेमंद तरीके से दिखाई देते हैं, और रिटेनर्स का पता लगाकर तथा लंबे समय तक जीवित रहने वाले ऑब्जेक्ट्स के lifecycle को साफ़ करके इन्हें ठीक किया जा सकता है। हम पहले ही VSCode में कुछ leak सुधार सुधार upstream कर चुके हैं और अधिक जोड़ने की तैयारी में हैं।
एक्सटेंशन क्रैश मेमोरी खत्म होने की वजह से भी हो सकते हैं, जिसे हम आंशिक रूप से प्रक्रिया पृथक्करण के ज़रिए कम करते हैं। मोटे तौर पर कहें तो, एक्सटेंशनों को उनकी अपनी पृथक प्रक्रियाओं में चलाकर हम यह सुनिश्चित करते हैं कि एक एक्सटेंशन में होने वाला क्रैश या लंबा कार्य दूसरे एक्सटेंशन की कार्यक्षमता को प्रभावित न करे। यह कुछ-कुछ वैसा ही है जैसे Chrome टैब्स को एक-दूसरे से पृथक रखता है, हालांकि इसकी कीमत थोड़ी अधिक सिस्टम मेमोरी के रूप में चुकानी पड़ती है।
रिग्रेशन को रोकना, तेज़ बने रहना
ऐप क्रैश को ठीक करना आम तौर पर नए क्रैश आने से रोकने की तुलना में ज़्यादा सीधा होता है, क्योंकि सुधार लक्षित होते हैं। रोकथाम के लिए हर डेवलपर को यह समझना ज़रूरी है कि उनका काम स्थिरता को कैसे प्रभावित करता है, वह भी एजेंट्स के साथ हासिल किए गए वेग से समझौता किए बिना। इसका मतलब है प्रक्रिया और टूलिंग—दोनों में निवेश करना।
हम इस दिशा में जिन कुछ तरीकों पर काम कर रहे हैं, उनमें शामिल हैं:
- हर प्रमुख OOM या ऐप क्रैश श्रेणी के लिए Bugbot नियम, जिनका हमने सामना किया है
- कौशल, जो हमें एजेंटिक कंप्यूटर उपयोग के ज़रिए अपने ऐप का स्ट्रेस परीक्षण आसानी से करने देते हैं
- ऐसी जोखिमभरी गलतियों को खत्म करना, जैसे लीकेज से बचने के लिए मैन्युअली प्रबंधित संसाधनों की जगह garbage collection का उपयोग करना
- पारंपरिक स्वचालित प्रदर्शन परीक्षण, जो हर कोड परिवर्तन के बाद चलते हैं
- मेट्रिक रिग्रेशन पर स्वचालित rollback जैसे मेथड्स के साथ पता लगाने की प्रक्रिया को पूरा करना
सॉफ़्टवेयर की नई पीढ़ी के लिए स्थिरता
एजेंटिक सॉफ़्टवेयर विकास नई सुविधाएँ शिप करना पहले से कहीं आसान बनाता है, लेकिन इसके साथ प्रदर्शन संबंधी समस्याएँ और बग्स आने का जोखिम भी बढ़ जाता है। वहीं, एप्लिकेशन की स्थिरता हासिल करने के लिए सॉफ़्टवेयर इंजीनियरिंग के वही मूलभूत सिद्धांत ज़रूरी हैं—बस उन्हें नई पीढ़ी के अनुरूप, समस्याओं को ठीक करने और रोकने वाली एजेंटिक रणनीतियों के साथ विकसित किया गया है।
उच्च गुणवत्ता वाला सॉफ़्टवेयर बनाना हमेशा से कठिन रहा है, और अब यह पहले से कहीं अधिक महत्वपूर्ण है। अगर आप इसके प्रति सचमुच जुनूनी हैं, तो हम आपसे सुनना चाहेंगे।